Asm4 Guide PDF
Asm4 Guide PDF
Asm4 Guide PDF
0
A Java bytecode engineering library
Eric Bruneton
Copyright
c 2007, 2011 Eric Bruneton
Redistribution and use in source (LYX format) and compiled forms (LATEX,
PDF, PostScript, HTML, RTF, etc), with or without modification, are per-
mitted provided that the following conditions are met:
1. Redistributions of source code (LYX format) must retain the above copy-
right notice, this list of conditions and the following disclaimer.
3. The name of the author may not be used to endorse or promote products
derived from this documentation without specific prior written permis-
sion.
1. Introduction 1
1.1. Motivations . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1
1.2. Overview . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 2
1.2.1. Scope . . . . . . . . . . . . . . . . . . . . . . . . . . . . 2
1.2.2. Model . . . . . . . . . . . . . . . . . . . . . . . . . . . . 3
1.2.3. Architecture . . . . . . . . . . . . . . . . . . . . . . . . 4
1.3. Organization . . . . . . . . . . . . . . . . . . . . . . . . . . . . 4
1.4. Acknowledgments . . . . . . . . . . . . . . . . . . . . . . . . . . 6
I. Core API 7
2. Classes 9
2.1. Structure . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 9
2.1.1. Overview . . . . . . . . . . . . . . . . . . . . . . . . . . 9
2.1.2. Internal names . . . . . . . . . . . . . . . . . . . . . . . 11
2.1.3. Type descriptors . . . . . . . . . . . . . . . . . . . . . . 11
2.1.4. Method descriptors . . . . . . . . . . . . . . . . . . . . . 12
2.2. Interfaces and components . . . . . . . . . . . . . . . . . . . . . 12
2.2.1. Presentation . . . . . . . . . . . . . . . . . . . . . . . . 12
2.2.2. Parsing classes . . . . . . . . . . . . . . . . . . . . . . . 14
2.2.3. Generating classes . . . . . . . . . . . . . . . . . . . . . 16
2.2.4. Transforming classes . . . . . . . . . . . . . . . . . . . . 18
2.2.5. Removing class members . . . . . . . . . . . . . . . . . 22
2.2.6. Adding class members . . . . . . . . . . . . . . . . . . . 23
2.2.7. Transformation chains . . . . . . . . . . . . . . . . . . . 25
2.3. Tools . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 25
2.3.1. Type . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 26
2.3.2. TraceClassVisitor . . . . . . . . . . . . . . . . . . . . 27
2.3.3. CheckClassAdapter . . . . . . . . . . . . . . . . . . . . 28
2.3.4. ASMifier . . . . . . . . . . . . . . . . . . . . . . . . . . 28
i
Contents
3. Methods 31
3.1. Structure . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 31
3.1.1. Execution model . . . . . . . . . . . . . . . . . . . . . . 31
3.1.2. Bytecode instructions . . . . . . . . . . . . . . . . . . . 33
3.1.3. Examples . . . . . . . . . . . . . . . . . . . . . . . . . . 35
3.1.4. Exception handlers . . . . . . . . . . . . . . . . . . . . . 38
3.1.5. Frames . . . . . . . . . . . . . . . . . . . . . . . . . . . 39
3.2. Interfaces and components . . . . . . . . . . . . . . . . . . . . . 41
3.2.1. Presentation . . . . . . . . . . . . . . . . . . . . . . . . 41
3.2.2. Generating methods . . . . . . . . . . . . . . . . . . . . 45
3.2.3. Transforming methods . . . . . . . . . . . . . . . . . . . 46
3.2.4. Stateless transformations . . . . . . . . . . . . . . . . . 48
3.2.5. Statefull transformations . . . . . . . . . . . . . . . . . 52
3.3. Tools . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 58
3.3.1. Basic tools . . . . . . . . . . . . . . . . . . . . . . . . . 58
3.3.2. AnalyzerAdapter . . . . . . . . . . . . . . . . . . . . . 61
3.3.3. LocalVariablesSorter . . . . . . . . . . . . . . . . . . 63
3.3.4. AdviceAdapter . . . . . . . . . . . . . . . . . . . . . . . 65
4. Metadata 67
4.1. Generics . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 67
4.1.1. Structure . . . . . . . . . . . . . . . . . . . . . . . . . . 67
4.1.2. Interfaces and components . . . . . . . . . . . . . . . . . 69
4.1.3. Tools . . . . . . . . . . . . . . . . . . . . . . . . . . . . 72
4.2. Annotations . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 72
4.2.1. Structure . . . . . . . . . . . . . . . . . . . . . . . . . . 73
4.2.2. Interfaces and components . . . . . . . . . . . . . . . . . 73
4.2.3. Tools . . . . . . . . . . . . . . . . . . . . . . . . . . . . 76
4.3. Debug . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 77
4.3.1. Structure . . . . . . . . . . . . . . . . . . . . . . . . . . 77
4.3.2. Interfaces and components . . . . . . . . . . . . . . . . . 78
4.3.3. Tools . . . . . . . . . . . . . . . . . . . . . . . . . . . . 80
5. Backward compatibility 81
5.1. Introduction . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 81
5.1.1. Backward compatibility contract . . . . . . . . . . . . . 82
5.1.2. An example . . . . . . . . . . . . . . . . . . . . . . . . . 83
5.2. Guidelines . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 84
5.2.1. Basic rule . . . . . . . . . . . . . . . . . . . . . . . . . . 84
ii
Contents
6. Classes 91
6.1. Interfaces and components . . . . . . . . . . . . . . . . . . . . . 91
6.1.1. Presentation . . . . . . . . . . . . . . . . . . . . . . . . 91
6.1.2. Generating classes . . . . . . . . . . . . . . . . . . . . . 92
6.1.3. Adding and removing class members . . . . . . . . . . . 93
6.2. Components composition . . . . . . . . . . . . . . . . . . . . . 95
6.2.1. Presentation . . . . . . . . . . . . . . . . . . . . . . . . 96
6.2.2. Patterns . . . . . . . . . . . . . . . . . . . . . . . . . . . 97
7. Methods 101
7.1. Interfaces and components . . . . . . . . . . . . . . . . . . . . . 101
7.1.1. Presentation . . . . . . . . . . . . . . . . . . . . . . . . 101
7.1.2. Generating methods . . . . . . . . . . . . . . . . . . . . 103
7.1.3. Transforming methods . . . . . . . . . . . . . . . . . . . 105
7.1.4. Stateless and statefull transformations . . . . . . . . . . 106
7.1.5. Global transformations . . . . . . . . . . . . . . . . . . . 109
7.2. Components composition . . . . . . . . . . . . . . . . . . . . . 112
7.2.1. Presentation . . . . . . . . . . . . . . . . . . . . . . . . 112
7.2.2. Patterns . . . . . . . . . . . . . . . . . . . . . . . . . . . 112
9. Metadata 127
9.1. Generics . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 127
9.2. Annotations . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 127
iii
Contents
A. Appendix 135
A.1. Bytecode instructions . . . . . . . . . . . . . . . . . . . . . . . 135
A.2. Subroutines . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 139
A.3. Attributes . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 141
A.4. Guidelines . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 143
A.5. Performances . . . . . . . . . . . . . . . . . . . . . . . . . . . . 145
Index 147
iv
1. Introduction
1.1. Motivations
1
1. Introduction
ASM is not the only tool for generating and transforming compiled Java
classes, but it is one of the most recent and efficient. It can be downloaded
from http://asm.objectweb.org. Its main advantages are the following:
• It has a simple, well designed and modular API that is easy to use.
• Its large user community can provide support for new users.
• Its open source license allows you to use it in almost any way you want.
1.2. Overview
1.2.1. Scope
The goal of the ASM library is to generate, transform and analyze compiled
Java classes, represented as byte arrays (as they are stored on disk and loaded
in the Java Virtual Machine). For this purpose ASM provides tools to read,
write and transform such byte arrays by using higher level concepts than bytes,
such as numeric constants, strings, Java identifiers, Java types, Java class
structure elements, etc. Note that the scope of the ASM library is strictly
limited to reading, writing, transforming and analyzing classes. In particular
the class loading process is out of scope.
1
the ASM name does not mean anything: it is just a reference to the __asm__ keyword in
C, which allows some functions to be implemented in assembly language.
2
1.2. Overview
1.2.2. Model
The ASM library provides two APIs for generating and transforming compiled
classes: the core API provides an event based representation of classes, while
the tree API provides an object based representation.
With the event based model a class is represented with a sequence of events,
each event representing an element of the class, such as its header, a field, a
method declaration, an instruction, etc. The event based API defines the set
of possible events and the order in which they must occur, and provides a class
parser that generates one event per element that is parsed, as well as a class
writer that generates compiled classes from sequences of such events.
With the object based model a class is represented with a tree of objects, each
object representing a part of the class, such as the class itself, a field, a method,
an instruction, etc. and each object having references to the objects that
represent its constituents. The object based API provides a way to convert
a sequence of events representing a class to the object tree representing the
same class and, vice versa, to convert an object tree to the equivalent event
sequence. In other words the object based API is built on top of the event
based API.
These two APIs can be compared to the Simple API for XML (SAX) and
Document Object Model (DOM) APIs for XML documents: the event based
API is similar to SAX, while the object based API is similar to DOM. The
object based API is built on top of the event based one, like DOM can be
provided on top of SAX.
ASM provides both APIs because there is no best API. Indeed each API has
its own advantages and drawbacks:
• The event based API is faster and requires less memory than the object
based API, since there is no need to create and store in memory a tree
of objects representing the class (the same difference also exists between
SAX and DOM).
• However implementing class transformations can be more difficult with
the event based API, since only one element of the class is available
at any given time (the element that corresponds to the current event),
while the whole class is available in memory with the object based API.
Note that the two APIs manage only one class at a time, and independently
of the others: no information about the class hierarchy is maintained, and if a
3
1. Introduction
1.2.3. Architecture
ASM applications have a strong architectural aspect. Indeed the event based
API is organized around event producers (the class parser), event consumers
(the class writer) and various predefined event filters, to which user defined
producers, consumers and filters can be added. Using this API is therefore a
two step process:
• assembling event producer, filter and consumer components into possibly
complex architectures,
• and then starting the event producers to run the generation or transfor-
mation process.
The object based API also has an architectural aspect: indeed class generator
or transformer components that operate on object trees can be composed, the
links between them representing the order of transformations.
Although most component architectures in typical ASM applications are quite
simple, it is possible to imagine complex architectures like the following, where
arrows represent event based or object based communications between class
parsers, writers or transformers, with possible conversions between the event
based and object based representations anywhere in the chain:
1.3. Organization
The ASM library is organized in several packages that are distributed in several
jar files:
4
1.3. Organization
Typographic conventions
5
1. Introduction
1.4. Acknowledgments
I would like to thank François Horn for his valuable remarks during the elabo-
ration of this document, which greatly improved its structure and readability.
6
Part I.
Core API
7
2. Classes
This chapter explains how to generate and transform compiled Java classes
with the core ASM API. It starts with a presentation of compiled classes
and then presents the corresponding ASM interfaces, components and tools to
generate and transform them, with many illustrative examples. The content
of methods, annotations and generics are explained in the next chapters.
2.1. Structure
2.1.1. Overview
The overall structure of a compiled class is quite simple. Indeed, unlike na-
tively compiled applications, a compiled class retains the structural informa-
tion and almost all the symbols from the source code. In fact a compiled class
contains:
• A section describing the modifiers (such as public or private), the
name, the super class, the interfaces and the annotations of the class.
• One section per field declared in this class. Each section describes the
modifiers, the name, the type and the annotations of a field.
• One section per method and constructor declared in this class. Each sec-
tion describes the modifiers, the name, the return and parameter types,
and the annotations of a method. It also contains the compiled code of
the method, in the form of a sequence of Java bytecode instructions.
There are however some differences between source and compiled classes:
• A compiled class describes only one class, while a source file can contain
several classes. For instance a source file describing a class with one inner
class is compiled in two class files: one for the main class and one for the
inner class. However the main class file contains references to its inner
classes, and inner classes defined inside methods contain a reference to
their enclosing method.
9
2. Classes
• A compiled class does not contain comments, of course, but can contain
class, field, method and code attributes that can be used to associate
additional information to these elements. Since the introduction of an-
notations in Java 5, which can be used for the same purpose, attributes
have become mostly useless.
• A compiled class does not contain a package and import section, so all
type names must be fully qualified.
10
2.1. Structure
Internal names are used only for types that are constrained to be class or
interface types. In all other situations, such as field types, Java types are
represented in compiled classes with type descriptors (see Figure 2.2).
The descriptors of the primitive types are single characters: Z for boolean, C
for char, B for byte, S for short, I for int, F for float, J for long and D
for double. The descriptor of a class type is the internal name of this class,
preceded by L and followed by a semicolon. For instance the type descriptor
of String is Ljava/lang/String;. Finally the descriptor of an array type is
a square bracket followed by the descriptor of the array element type.
11
2. Classes
Once you know how type descriptors work, understanding method descriptors
is easy. For instance (I)I describes a method that takes one argument of type
int, and returns an int. Figure 2.3 gives several method descriptor examples.
2.2.1. Presentation
The ASM API for generating and transforming compiled classes is based on
the ClassVisitor abstract class (see Figure 2.4). Each method in this class
corresponds to the class file structure section of the same name (see Figure
2.1). Simple sections are visited with a single method call whose arguments
describe their content, and which returns void. Sections whose content can be
of arbitrary length and complexity are visited with a initial method call that
returns an auxiliary visitor class. This is the case of the visitAnnotation,
visitField and visitMethod methods, which return an AnnotationVisitor,
a FieldVisitor and a MethodVisitor respectively.
The same principles are used recursively for these auxiliary classes. For ex-
ample each method in the FieldVisitor abstract class (see Figure 2.5) corre-
sponds to the class file sub structure of the same name, and visitAnnotation
12
2.2. Interfaces and components
The methods of the ClassVisitor class must be called in the following order,
specified in the Javadoc of this class:
visit visitSource? visitOuterClass? ( visitAnnotation | visitAttribute )*
( visitInnerClass | visitField | visitMethod )*
visitEnd
This means that visit must be called first, followed by at most one call to
visitSource, followed by at most one call to visitOuterClass, followed by
13
2. Classes
14
2.2. Interfaces and components
return null;
}
public void visitAttribute(Attribute attr) {
}
public void visitInnerClass(String name, String outerName,
String innerName, int access) {
}
public FieldVisitor visitField(int access, String name, String desc,
String signature, Object value) {
System.out.println(" " + desc + " " + name);
return null;
}
public MethodVisitor visitMethod(int access, String name,
String desc, String signature, String[] exceptions) {
System.out.println(" " + name + desc);
return null;
}
public void visitEnd() {
System.out.println("}");
}
}
The second line creates a ClassReader to parse the Runnable class. The
accept method called at the last line parses the Runnable class bytecode
and calls the corresponding ClassVisitor methods on cp. The result is the
following output:
Note that there are several ways to construct a ClassReader instance. The
class that must be read can be specified by name, as above, or by value, as a
byte array or as an InputStream. An input stream to read the content of a
class can be obtained with the ClassLoader’s getResourceAsStream method
with:
15
2. Classes
The first line creates a ClassWriter instance that will actually build the byte
array representation of the class (the constructor argument is explained in the
next chapter).
The call to the visit method defines the class header. The V1_5 argument is a
constant defined, like all other ASM constants, in the ASM Opcodes interface.
It specifies the class version, Java 1.5. The ACC_XXX constants are flags that
correspond to Java modifiers. Here we specify that the class is an interface, and
that it is public and abstract (because it cannot be instantiated). The next
argument specifies the class name, in internal form (see section 2.1.2). Recall
that compiled classes do not contain a package or import section, so all class
names must be fully qualified. The next argument corresponds to generics (see
section 4.1). In our case it is null because the interface is not parameterized
by a type variable. The fifth argument is the super class, in internal form
(interface classes implicitly inherit from Object). The last argument is an
array of the interfaces that are extended, specified by their internal names.
16
2.2. Interfaces and components
The next three calls to visitField are similar, and are used to define the
three interface fields. The first argument is a set of flags that correspond to
Java modifiers. Here we specify that the fields are public, final and static.
The second argument is the name of the field, as it appears in source code.
The third argument is the type of the field, in type descriptor form. Here the
fields are int fields, whose descriptor is I. The fourth argument corresponds to
generics. In our case it is null because the field types are not using generics.
The last argument is the field’s constant value: this argument must be used
only for truly constant fields, i.e. final static fields. For other fields it
must be null. Since there are no annotations here, we call the visitEnd
method of the returned FieldVisitor immediately, i.e. without any call to
its visitAnnotation or visitAttribute methods.
The visitMethod call is used to define the compareTo method. Here again
the first argument is a set of flags that correspond to Java modifiers. The
second argument is the method name, as it appears in source code. The third
argument is the descriptor of the method. The fourth argument corresponds to
generics. In our case it is null because the method is not using generics. The
last argument is an array of the exceptions that can be thrown by the method,
specified by their internal names. Here it is null because the method does not
declare any exception. The visitMethod method returns a MethodVisitor
(see Figure 3.4), which can be used to define the method’s annotations and
attributes, and most importantly the method’s code. Here, since there are no
annotations and since the method is abstract, we call the visitEnd method
of the returned MethodVisitor immediately.
Finally a last call to visitEnd is used to inform cw that the class is finished
and a call to toByteArray is used to retrieve it as a byte array.
The previous byte array can be stored in a Comparable.class file for future
use. Alternatively it can be loaded dynamically with a ClassLoader. One
method is to define a ClassLoader subclass whose defineClass method is
public:
class MyClassLoader extends ClassLoader {
public Class defineClass(String name, byte[] b) {
return defineClass(name, b, 0, b.length);
}
}
17
2. Classes
In fact the way of using your generated classes depends on the context, and
is out of scope of the ASM API. If you are writing a compiler, the class
generation process will be driven by an abstract syntax tree representing the
program to be compiled, and the generated classes will be stored on disk. If
you are writing a dynamic proxy class generator or aspect weaver you will use,
in one way or another, a ClassLoader.
So far the ClassReader and ClassWriter components were used alone. The
events were produced “by hand” and consumed directly by a ClassWriter or,
symetrically, they were produced by a ClassReader and consumed “by hand”,
i.e. by a custom ClassVisitor implementation. Things start to become really
interesting when these components are used together. The first step is to direct
the events produced by a ClassReader to a ClassWriter. The result is that
the class parsed by the class reader is reconstructed by the class writer:
byte[] b1 = ...;
ClassWriter cw = new ClassWriter(0);
ClassReader cr = new ClassReader(b1);
cr.accept(cw, 0);
byte[] b2 = cw.toByteArray(); // b2 represents the same class as b1
18
2.2. Interfaces and components
This is not really interesting in itself (there are easier ways to copy a byte
array!), but wait. The next step is to introduce a ClassVisitor between the
class reader and the class writer:
byte[] b1 = ...;
ClassWriter cw = new ClassWriter(0);
// cv forwards all events to cw
ClassVisitor cv = new ClassVisitor(ASM4, cw) { };
ClassReader cr = new ClassReader(b1);
cr.accept(cv, 0);
byte[] b2 = cw.toByteArray(); // b2 represents the same class as b1
The result does not change, however, because the ClassVisitor event filter
does not filter anything. But it is now sufficient to filter some events, by
overriding some methods, in order to be able to transform a class. For example,
consider the following ClassVisitor subclass:
public class ChangeVersionAdapter extends ClassVisitor {
public ChangeVersionAdapter(ClassVisitor cv) {
super(ASM4, cv);
}
@Override
public void visit(int version, int access, String name,
String signature, String superName, String[] interfaces) {
cv.visit(V1_5, access, name, signature, superName, interfaces);
}
}
This class overrides only one method of the ClassVisitor class. As a conse-
quence all calls are forwarded unchanged to the class visitor cv passed to the
constructor, except calls to the visit method, which are forwarded with a
modified class version number. The corresponding sequence diagram is shown
in Figure 2.7.
19
2. Classes
By modifying other arguments of the visit method you can implement other
transformations than just changing the class version. For instance you can add
an interface to the list of implemented interfaces. It is also possible to change
the name of the class, but this requires much more than just changing the name
argument in the visit method. Indeed the name of the class can appear in
many different places inside a compiled class, and all these occurrences must
be changed to really rename the class.
Optimization
The previous transformation changes only four bytes in the original class.
However, with the above code, b1 is fully parsed and the corresponding events
are used to construct b2 from scratch, which is not very efficient. It would be
much more efficient to copy the parts of b1 that are not transformed directly
into b2, without parsing these parts and without generating the corresponding
events. ASM automatically performs this optimization for methods:
• If a ClassReader component detects that a MethodVisitor returned by
the ClassVisitor passed as argument to its accept method comes from
a ClassWriter, this means that the content of this method will not be
transformed, and will in fact not even be seen by the application.
• In this case the ClassReader component does not parse the content of
this method, does not generate the corresponding events, and just copies
20
2.2. Interfaces and components
Thanks to this optimization the above code is two times faster than the pre-
vious one, because ChangeVersionAdapter does not transform any method.
For common class transformations, which transform some or all methods, the
speedup is smaller, but is still noticeable: it is indeed of the order of 10 to
20%. Unfortunately this optimization requires to copy all the constants de-
fined in the original class into the transformed one. This is not a problem
for tranformations that add fields, methods or instructions, but this leads to
bigger class files, compared to the unoptimized case, for transformations that
remove or rename many class elements. It is therefore recommanded to use
this optimization only for “additive” transformations.
21
2. Classes
The method used to transform the class version in the previous section can
of course be applied to other methods of the ClassVisitor class. For in-
stance, by changing the access or name argument in the visitField and
visitMethod methods, you can change the modifiers or the name of a field or
of a method. Furthermore, instead of forwarding a method call with modified
arguments, you can choose to not forward this call at all. The effect is that
the corresponding class element is removed.
For example the following class adapter removes the information about outer
and inner classes, as well as the name of the source file from which the class
was compiled (the resulting class remains fully functional, because these ele-
ments are only used for debugging purposes). This is done by not forwarding
anything in the appropriate visit methods:
public class RemoveDebugAdapter extends ClassVisitor {
public RemoveDebugAdapter(ClassVisitor cv) {
super(ASM4, cv);
}
@Override
public void visitSource(String source, String debug) {
}
@Override
public void visitOuterClass(String owner, String name, String desc) {
}
@Override
public void visitInnerClass(String name, String outerName,
String innerName, int access) {
}
}
This strategy does not work for fields and methods, because the visitField
and visitMethod methods must return a result. In order to remove a field or
method, you must not forward the method call, and return null to the caller.
For example the following class adapter removes a single method, specified
by its name and by its descriptor (the name is not sufficient to identify a
method, because a class can contain several methods of the same name but
with different parameters):
public class RemoveMethodAdapter extends ClassVisitor {
private String mName;
private String mDesc;
public RemoveMethodAdapter(
ClassVisitor cv, String mName, String mDesc) {
super(ASM4, cv);
this.mName = mName;
22
2.2. Interfaces and components
this.mDesc = mDesc;
}
@Override
public MethodVisitor visitMethod(int access, String name,
String desc, String signature, String[] exceptions) {
if (name.equals(mName) && desc.equals(mDesc)) {
// do not delegate to next visitor -> this removes the method
return null;
}
return cv.visitMethod(access, name, desc, signature, exceptions);
}
}
Instead of forwarding fewer calls than you receive, you can “forward” more,
which has the effect of adding class elements. The new calls can be inserted
at several places between the original method calls, provided that the order in
which the various visitXxx methods must be called is respected (see section
2.2.1).
For instance, if you want to add a field to a class you have to insert a new
call to visitField between the original method calls, and you must put this
new call in one of the visit method of your class adapter. You cannot do
this in the visit method, for example, because this may result in a call to
visitField followed by visitSource, visitOuterClass, visitAnnotation
or visitAttribute, which is not valid. You cannot put this new call in
the visitSource, visitOuterClass, visitAnnotation or visitAttribute
methods, for the same reason. The only possibilities are the visitInnerClass,
visitField, visitMethod or visitEnd methods.
If you put the new call in the visitEnd method the field will always be added
(unless you add explicit conditions), because this method is always called. If
you put it in visitField or visitMethod, several fields will be added: one
per field or method in the original class. Both solutions can make sense; it
depends on what you need. For instance you can add a single counter field to
count the invocations on an object, or one counter per method, to count the
invocations of each method separately.
Note: in fact the only truly correct solution is to add new members by making
additional calls in the visitEnd method. Indeed a class must not contain
duplicate members, and the only way to be sure that a new member is
23
2. Classes
unique is to compare it with all the existing members, which can only
be done once they have all been visited, i.e. in the visitEnd method.
This is rather constraining. Using generated names that are unlikely
to be used by a programmer, such as _counter$ or _4B7F_ is sufficient
in practice to avoid duplicate members without having to add them in
visitEnd. Note that, as discussed in the first chapter, the tree API does
not have this limitation: it is possible to add new members at any time
inside a transformation with this API.
In order to illustrate the above discussion, here is a class adapter that adds a
field to a class, unless this field already exists:
The field is added in the visitEnd method. The visitField method is not
overridden to modify existing fields or to remove a field, but just to detect if
24
2.3. Tools
the field we want to add already exists or not. Note the fv != null test in the
visitEnd method, before calling fv.visitEnd(): this is because, as we have
seen in the previous section, a class visitor can return null in visitField.
2.3. Tools
25
2. Classes
2.3.1. Type
As you have seen in the previous sections, the ASM API exposes Java types
as they are stored in compiled classes, i.e. as internal names or type descrip-
tors. It would be possible to expose them as they appear in source code,
to make code more readable. But this would require systematic conversions
between the two representations in ClassReader and ClassWriter, which
would degrade performances. This is why ASM does not transparently trans-
form internal names and type descriptors to their equivalent source code form.
However it provides the Type class for doing that manually when necessary.
A Type object represents a Java type, and can be constructed either from a
type descriptor or from a Class object. The Type class also contains static
variables representing the primitive types. For example Type.INT_TYPE is the
Type object representing the int type.
The getInternalName method returns the internal name of a Type. For ex-
ample Type.getType(String.class).getInternalName() gives the internal
name of the String class, i.e. "java/lang/String". This method must be
used only for class or interface types.
The getDescriptor method returns the descriptor of a Type. So, for ex-
ample, instead of using "Ljava/lang/String;" in your code you could use
Type.getType(String.class).getDescriptor(). Or, instead of using I,
you could use Type.INT_TYPE.getDescriptor().
A Type object can also represent a method type. Such objects can be con-
structed either from a method descriptor or from a Method object. The
getDescriptor method then returns the method descriptor corresponding to
this type. In addition, the getArgumentTypes and getReturnType methods
can be used to get the Type objects corresponding to the argument types and
26
2.3. Tools
2.3.2. TraceClassVisitor
This code creates a TraceClassVisitor that delegates all the calls it receives
to cw, and that prints a textual representation of these calls to printWriter.
For example, using a TraceClassVisitor in the example of section 2.2.3 would
give:
// class version 49.0 (49)
// access flags 1537
public abstract interface pkg/Comparable implements pkg/Mesurable {
// access flags 25
public final static I LESS = -1
// access flags 25
public final static I EQUAL = 0
// access flags 25
public final static I GREATER = 1
// access flags 1025
public abstract compareTo(Ljava/lang/Object;)I
}
27
2. Classes
2.3.3. CheckClassAdapter
The ClassWriter class does not check that its methods are called in the
appropriate order and with valid arguments. It is therefore possible to gener-
ate invalid classes that will be rejected by the Java Virtual Machine verifier.
In order to detect some of these errors as soon as possible, it is possible to
use the CheckClassAdapter class. Like TraceClassVisitor, this class ex-
tends the ClassVisitor class, and delegates all calls to its method to another
ClassVisitor, for instance a TraceClassVisitor or a ClassWriter. How-
ever, instead of printing a textual representation of the visited class, this
class checks that its methods are called in the appropriate order, and with
valid arguments, before delegating to the next visitor. In case of errors an
IllegalStateException or IllegalArgumentException is thrown.
In order to check a class, print a textual representation of this class, and finally
create a byte array representation, you should use something like:
ClassWriter cw = new ClassWriter(0);
TraceClassVisitor tcv = new TraceClassVisitor(cw, printWriter);
CheckClassAdapter cv = new CheckClassAdapter(tcv);
cv.visit(...);
...
cv.visitEnd();
byte b[] = cw.toByteArray();
Note that if you chain these class visitors in a different order, the operations
they perform will be done in a different order too. For example, with the
following code, the checks will take place after the trace:
ClassWriter cw = new ClassWriter(0);
CheckClassAdapter cca = new CheckClassAdapter(cw);
TraceClassVisitor cv = new TraceClassVisitor(cca, printWriter);
2.3.4. ASMifier
28
2.3. Tools
29
2. Classes
30
3. Methods
This chapter explains how to generate and transform compiled methods with
the core ASM API. It starts with a presentation of compiled methods and then
presents the corresponding ASM interfaces, components and tools to generate
and transform them, with many illustrative examples.
3.1. Structure
31
3. Methods
not confuse the operand stack and the thread’s execution stack: each frame
in the execution stack contains its own operand stack.
The size of the local variables and operand stack parts depends on the method’s
code. It is computed at compile time and is stored along with the bytecode
instructions in compiled classes. As a consequence, all the frames that cor-
respond to the invocation of a given method have the same size, but frames
that correspond to different methods can have different sizes for their local
variables and operand stack parts.
Figure 3.1 shows a sample execution stack with 3 frames. The first frame
contains 3 local variables, its operand stack has a maximum size of 4, and
it contains two values. The second frame contains 2 local variables, and two
values in its operand stack. Finally the third frame, on top of the execution
stack, contains 4 local variables and two operands.
When it is created, a frame is initialized with an empty stack, and its local vari-
ables are initialized with the target object this (for non static methods) and
with the method’s arguments. For instance, calling the method a.equals(b)
creates a frame with an empty stack and with the first two local variables
initialized to a and b (other local variables are uninitialized).
Each slot in the local variables and operand stack parts can hold any Java
value, except long and double values. These values require two slots. This
complicates the management of local variables: for instance the ith method
argument is not necessarily stored in local variable i. For example, calling
Math.max(1L, 2L) creates a frame with the 1L value in the first two local
variable slots, and with the value 2L in the third and fourth slots.
32
3.1. Structure
33
3. Methods
means that the type of a local variable, i.e. the type of the value stored in this
local variable, can change during the execution of a method.
As said above, all other bytecode instructions work on the operand stack only.
They can be grouped in the following categories (see appendix A.1):
Stack These instructions are used to manipulate values on the stack: POP
pops the value on top of the stack, DUP pushes a copy of the top stack
value, SWAP pops two values and pushes them in the reverse order, etc.
Constants These instructions push a constant value on the operand stack:
ACONST_NULL pushes null, ICONST_0 pushes the int value 0, FCONST_0
pushes 0f, DCONST_0 pushes 0d, BIPUSH b pushes the byte value b,
SIPUSH s pushes the short value s, LDC cst pushes the arbitrary int,
float, long, double, String, or class1 constant cst, etc.
Arithmetic and logic These instructions pop numeric values from the operand
stack combine them and push the result on the stack. They do not have
any argument. xADD, xSUB, xMUL, xDIV and xREM correspond to the +,
-, *, / and % operations, where x is either I, L, F or D. Similarly there
are other instructions corresponding to <<, >>, >>>, |, & and ^, for int
and long values.
Casts These instructions pop a value from the stack, convert it to another
type, and push the result back. They correspond to cast expressions in
Java. I2F, F2D, L2D, etc. convert numeric values from one numeric type
to another. CHECKCAST t converts a reference value to the type t.
Objects These instructions are used to create objects, lock them, test their
type, etc. For instance the NEW type instruction pushes a new object of
type type on the stack (where type is an internal name).
Fields These instructions read or write the value of a field. GETFIELD owner
name desc pops an object reference, and pushes the value of its name
field. PUTFIELD owner name desc pops a value and an object reference,
and stores this value in its name field. In both cases the object must
be of type owner, and its field must be of type desc. GETSTATIC and
PUTSTATIC are similar instructions, but for static fields.
Methods These instructions invoke a method or a constructor. They pop as
many values as there are method arguments, plus one value for the target
object, and push the result of the method invocation. INVOKEVIRTUAL
1
this corresponds to the identifier.class Java syntax.
34
3.1. Structure
owner name desc invokes the name method defined in class owner,
and whose method descriptor is desc. INVOKESTATIC is used for static
methods, INVOKESPECIAL for private methods and constructors, and
INVOKEINTERFACE for methods defined in interfaces. Finally, for Java
7 classes, INVOKEDYNAMIC is used for the new dynamic method invoca-
tion mechanism.
Arrays These instructions are used to read and write values in arrays. The
xALOAD instructions pop an index and an array, and push the value of
the array element at this index. The xASTORE instructions pop a value,
an index and an array, and store this value at that index in the array.
Here x can be I, L, F, D or A, but also B, C or S.
Jumps These instructions jump to an arbitrary instruction if some condition
is true, or unconditionally. They are used to compile if, for, do, while,
break and continue instructions. For instance IFEQ label pops an int
value from the stack, and jumps to the instruction designed by label
if this value is 0 (otherwise execution continues normally to the next
instruction). Many other jump instructions exist, such as IFNE or IFGE.
Finally TABLESWITCH and LOOKUPSWITCH correspond to the switch Java
instruction.
Return Finally the xRETURN and RETURN instructions are used to terminate
the execution of a method and to return its result to the caller. RETURN is
used for methods that return void, and xRETURN for the other methods.
3.1.3. Examples
Lets look at some basic examples to get a more concrete sense of how bytecode
instructions work. Consider the following bean class:
package pkg;
public class Bean {
private int f;
public int getF() {
return this.f;
}
public void setF(int f) {
this.f = f;
}
}
35
3. Methods
ALOAD 0
GETFIELD pkg/Bean f I
IRETURN
The first instruction reads the local variable 0, which was initialized to this
during the creation of the frame for this method call, and pushes this value on
the operand stack. The second instruction pops this value from the stack, i.e.
this, and pushes the f field of this object, i.e. this.f. The last instruction
pops this value from the stack, and returns it to the caller. The successive
states of the execution frame for this method are shown in Figure 3.2.
Figure 3.2.: Successive frame states for the getF method: a) initial state, b)
after ALOAD 0 and c) after GETFIELD
The first instruction pushes this on the operand stack, as before. The second
instruction pushes the local variable 1, which was initialized with the f argu-
ment value during the creation of the frame for this method call. The third
instruction pops these two values and stores the int value in the f field of
the referenced object, i.e. in this.f. The last instruction, which is implicit
in the source code but which is mandatory in the compiled code, destroys the
current execution frame and returns to the caller. The successive states of the
execution frame for this method are shown in Figure 3.3.
The Bean class also has a default public constructor which is generated by
the compiler, since no explicit constructor was defined by the programmer.
This default public constructor is generated as Bean() { super(); }. The
bytecode of this constructor is the following:
ALOAD 0
36
3.1. Structure
Figure 3.3.: Successive frame states for the setF method: a) initial state, b)
after ALOAD 0, c) after ILOAD 1 and d) after PUTFIELD
The first instruction pushes this on the operand stack. The second instruc-
tion pops this value from the stack, and calls the <init> method defined in
the Object class. This corresponds to the super() call, i.e. a call to the
constructor of the super class, Object. You can see here that constructors are
named differently in compiled and source classes: in compiled classes they are
always named <init>, while in source classes they have the name of the class
in which they are defined. Finally the last instruction returns to the caller.
Now let us consider a slightly more complex setter method:
public void checkAndSetF(int f) {
if (f >= 0) {
this.f = f;
} else {
throw new IllegalArgumentException();
}
}
37
3. Methods
end:
RETURN
The first instruction pushes the local variable 1, initialized to f, on the operand
stack. The IFLT instruction pops this value from the stack, and compares it
to 0. If it is Less Than (LT) 0, it jumps to the instruction designated by the
label label, otherwise it does nothing and the execution continues to the next
instruction. The next three instructions are the same instructions as in the
setF method. The GOTO instruction unconditionally jumps to the instruction
designated by the end label, which is the RETURN instruction. The instructions
between the label and end labels create and throw an exception: the NEW
instruction creates an exception object and pushes it on the operand stack.
The DUP instruction duplicates this value on the stack. The INVOKESPECIAL
instruction pops one of these two copies and calls the exception constructor
on it. Finally the ATHROW instruction pops the remaining copy and throws it
as an exception (so the execution does not continue to the next instruction).
38
3.1. Structure
The code between the try and catch labels corresponds to the try block,
while the code after the catch label corresponds to the catch block. The
TRYCATCHBLOCK line specifies an exception handler that covers the range be-
tween the try and catch labels, with a handler starting at the catch label,
and for exceptions whose class is a subclass of InterruptedException. This
means that if such an exception is thrown anywhere between try and catch
the stack is cleared, the exception is pushed on this empty stack, and execution
continues at catch .
3.1.5. Frames
For example, if we consider the getF method of the previous section, we can
define three stack map frames giving the state of the execution frame just
before ALOAD, just before GETFIELD, and just before IRETURN. These three
stack map frames correspond to the three cases shown in Figure 3.2 and can
be described as follows, where the types between the first square brackets
correspond to the local variables, and the others to the operand stack:
39
3. Methods
40
3.2. Interfaces and components
at all, because it can easily be deduced from the method parameter types. In
the case of the checkAndSetF method the two frames that must be stored are
equal and are equal to the initial frame, so they are stored as the single byte
value designated by the F_SAME mnemonic. These frames can be represented
just before their associated bytecode instruction. This gives the final bytecode
for the checkAndSetF method:
ILOAD 1
IFLT label
ALOAD 0
ILOAD 1
PUTFIELD pkg/Bean f I
GOTO end
label:
F_SAME
NEW java/lang/IllegalArgumentException
DUP
INVOKESPECIAL java/lang/IllegalArgumentException <init> ()V
ATHROW
end:
F_SAME
RETURN
3.2.1. Presentation
The ASM API for generating and transforming compiled methods is based on
the MethodVisitor abstract class (see Figure 3.4), which is returned by the
ClassVisitor’s visitMethod method. In addition to some methods related to
annotations and debug information, which are explained in the next chapter,
this class defines one method per bytecode instruction category, based on the
number and type of arguments of these instructions (these categories do not
correspond to the ones presented in section 3.1.2). These methods must be
called in the following order (with some additional constraints specified in the
Javadoc of the MethodVisitor interface):
visitAnnotationDefault?
( visitAnnotation | visitParameterAnnotation | visitAttribute )*
( visitCode
( visitTryCatchBlock | visitLabel | visitFrame | visitXxx Insn |
visitLocalVariable | visitLineNumber )*
visitMaxs )?
visitEnd
41
3. Methods
This means that annotations and attributes, if any, must be visited first, fol-
lowed by the method’s bytecode, for non abstract methods. For these meth-
ods the code must be visited in sequential order, between exactly one call to
visitCode and exactly one call to visitMaxs.
The visitCode and visitMaxs methods can therefore be used to detect the
start and end of the method’s bytecode in a sequence of events. Like for
classes, the visitEnd method must be called last, and is used to detect the
42
3.2. Interfaces and components
Note that it is not necessary to finish one method in order to start visiting
another one. In fact MethodVisitor instances are completely independent
and can be used in any order (as long as cv.visitEnd() has not been called):
ClassVisitor cv = ...;
cv.visit(...);
MethodVisitor mv1 = cv.visitMethod(..., "m1", ...);
mv1.visitCode();
mv1.visitInsn(...);
...
MethodVisitor mv2 = cv.visitMethod(..., "m2", ...);
mv2.visitCode();
mv2.visitInsn(...);
...
mv1.visitMaxs(...);
mv1.visitEnd();
...
mv2.visitMaxs(...);
mv2.visitEnd();
cv.visitEnd();
43
3. Methods
ClassWriter options
As we have seen in section 3.1.5, computing the stack map frames for a method
is not very easy: you have to compute all the frames, find the frames that
correspond to jump targets or that follow unconditional jumps, and finally
compress these remaining frames. Likewise, computing the size of the local
variables and operand stack parts for a method is easier but still not very easy.
Hopefully ASM can compute this for you. When you create a ClassWriter
you can specify what must be automatically computed:
• with new ClassWriter(0) nothing is automatically computed. You
have to compute yourself the frames and the local variables and operand
stack sizes.
• with new ClassWriter(ClassWriter.COMPUTE_MAXS) the sizes of the
local variables and operand stack parts are computed for you. You must
still call visitMaxs, but you can use any arguments: they will be ignored
and recomputed. With this option you still have to compute the frames
yourself.
• with new ClassWriter(ClassWriter.COMPUTE_FRAMES) everything is
computed automatically. You don’t have to call visitFrame, but you
must still call visitMaxs (arguments will be ignored and recomputed).
Using these options is convenient but this has a cost: the COMPUTE_MAXS op-
tion makes a ClassWriter 10% slower, and using the COMPUTE_FRAMES option
makes it two times slower. This must be compared to the time it would take
to compute this yourself: in specific situations there are often easier and faster
algorithms for computing this, compared to the algorithm used in ASM, which
must handle all cases.
Note that if you choose to compute the frames yourself, you can let the
ClassWriter class do the compression step for you. For this you just have to
visit your uncompressed frames with visitFrame(F_NEW, nLocals, locals,
44
3.2. Interfaces and components
nStack, stack), where nLocals and nStack are the number of locals and
the operand stack size, and locals and stack are arrays containing the cor-
responding types (see the Javadoc for more details).
Note also that, in order to compute frames automatically, it is sometimes
necessary to compute the common super class of two given classes. By default
the ClassWriter class computes this, in the getCommonSuperClass method,
by loading the two classes into the JVM and by using the reflection API. This
can be a problem if you are generating several classes that reference each other,
because the referenced classes may not yet exist. In this case you can override
the getCommonSuperClass method to solve this problem.
The bytecode of the getF method defined in section 3.1.3 can be generated
with the following method calls, if mv is a MethodVisitor:
mv.visitCode();
mv.visitVarInsn(ALOAD, 0);
mv.visitFieldInsn(GETFIELD, "pkg/Bean", "f", "I");
mv.visitInsn(IRETURN);
mv.visitMaxs(1, 1);
mv.visitEnd();
The first call starts the bytecode generation. It is followed by three calls that
generate the three instructions of this method (as you can see the mapping be-
tween the bytecode and the ASM API is quite simple). The call to visitMaxs
must be done after all the instructions have been visited. It is used to define
the sizes of the local variables and operand stack parts for the execution frame
of this method. As we saw in section 3.1.3, these sizes are 1 slot for each part.
Finally the last call is used to end the generation of the method.
The bytecode of the setF method and of the constructor can be generated in
a similar way. A more interesting example is the checkAndSetF method:
mv.visitCode();
mv.visitVarInsn(ILOAD, 1);
Label label = new Label();
mv.visitJumpInsn(IFLT, label);
mv.visitVarInsn(ALOAD, 0);
mv.visitVarInsn(ILOAD, 1);
mv.visitFieldInsn(PUTFIELD, "pkg/Bean", "f", "I");
Label end = new Label();
mv.visitJumpInsn(GOTO, end);
45
3. Methods
mv.visitLabel(label);
mv.visitFrame(F_SAME, 0, null, 0, null);
mv.visitTypeInsn(NEW, "java/lang/IllegalArgumentException");
mv.visitInsn(DUP);
mv.visitMethodInsn(INVOKESPECIAL,
"java/lang/IllegalArgumentException", "<init>", "()V");
mv.visitInsn(ATHROW);
mv.visitLabel(end);
mv.visitFrame(F_SAME, 0, null, 0, null);
mv.visitInsn(RETURN);
mv.visitMaxs(2, 2);
mv.visitEnd();
Between the visitCode and visitEnd calls you can see method calls that map
exactly to the bytecode shown at the end of section 3.1.5: one call per instruc-
tion, label or frame (the only exceptions are the declaration and construction
of the label and end Label objects).
Note: a Label object designates the instruction that follows the visitLabel
call for this label. For example end designates the RETURN instruction,
and not the frame that is visited just after, since this is not an instruc-
tion. It is perfectly legal to have several labels designating the same
instruction, but a label must designate exactly one instruction. In other
words it is possible to have successive calls to visitLabel with different
labels, but a label used in an instruction must be visited exactly once
with visitLabel. A last constraint is that labels can not be shared:
each method must have its own labels.
You should now have guessed that methods can be transformed like classes,
i.e. by using a method adapter that forwards the method calls it receives with
some modifications: changing arguments can be used to change individual in-
structions, not forwarding a received call removes an instruction, and inserting
calls between the received ones adds new instructions. The MethodVisitor
class provides a basic implementation of such a method adapter, which does
nothing else than just forwarding all the method calls it receives.
In order to understand how method adapters can be used, let’s consider a very
simple adapter that removes the NOP instructions inside methods (they can be
removed without problems since they do nothing):
public class RemoveNopAdapter extends MethodVisitor {
46
3.2. Interfaces and components
In other words the class adapter just builds a method adapter encapsulating
the method visitor returned by the next class visitor in the chain, and returns
this adapter. The effect is the construction of a method adapter chain that is
similar to the class adapter chain (see Figure 3.5).
Note however that this is not mandatory: it is perfectly possible to build
a method adapter chain that is not similar to the class adapter chain. Each
method can even have a different method adapter chain. For instance the class
adapter could choose to remove NOPs only in methods and not in constructors.
This can be done as follows:
...
mv = cv.visitMethod(access, name, desc, signature, exceptions);
if (mv != null && !name.equals("<init>")) {
mv = new RemoveNopAdapter(mv);
}
...
In this case the adapter chain is shorter for constructors. On the contrary, the
adapter chain for constructors could have been longer, with several method
47
3. Methods
Now that we have seen how method adapters can be used and combined inside
a class adapter, let’s see how to implement more interesting adapters than
RemoveNopAdapter.
Let’s suppose we want to measure the time spent in each class of a program.
We need to add a static timer field in each class, and we need to add the
execution time of each method of this class to this timer field. In other words
we want to transform a class such as C:
public class C {
public void m() throws Exception {
Thread.sleep(100);
48
3.2. Interfaces and components
}
}
into this:
public class C {
public static long timer;
public void m() throws Exception {
timer -= System.currentTimeMillis();
Thread.sleep(100);
timer += System.currentTimeMillis();
}
}
We see that we must add four instructions at the beginning of the method,
and four other instructions before the return instruction. We also need to
update the maximum operand stack size. The beginning of the method’s code
is visited with the visitCode method. We can therefore add the first four
instructions by overriding this method in our method adapter:
public void visitCode() {
mv.visitCode();
mv.visitFieldInsn(GETSTATIC, owner, "timer", "J");
mv.visitMethodInsn(INVOKESTATIC, "java/lang/System",
"currentTimeMillis", "()J");
mv.visitInsn(LSUB);
mv.visitFieldInsn(PUTSTATIC, owner, "timer", "J");
}
49
3. Methods
where owner must be set to the name of the class that is being transformed.
We must now add four other instructions before any RETURN, but also before
any xRETURN or before ATHROW, which are all the instructions that terminate
the method’s execution. These instructions do not have any argument, and
are therefore visited in the visitInsn method. We can then override this
method in order to add our instructions:
public void visitInsn(int opcode) {
if ((opcode >= IRETURN && opcode <= RETURN) || opcode == ATHROW) {
mv.visitFieldInsn(GETSTATIC, owner, "timer", "J");
mv.visitMethodInsn(INVOKESTATIC, "java/lang/System",
"currentTimeMillis", "()J");
mv.visitInsn(LADD);
mv.visitFieldInsn(PUTSTATIC, owner, "timer", "J");
}
mv.visitInsn(opcode);
}
Finally we must update the maximum operand stack size. The instructions
that we add push two long values, and therefore require four slots on the
operand stack. At the beginning of the method the operand stack is initially
empty, so we know that the four instructions added at the beginning require
a stack of size 4. We also know that our inserted code leaves the stack state
unchanged (because it pops as many values as it pushes). As a consequence,
if the original code requires a stack of size s, the maximum stack size needed
by the transformed method is max(4, s). Unfortunately we also add four
instructions before the return instructions, and here we do not know the size
of the operand stack just before these instructions. We just know that it is
less than or equal to s. As a consequence we can just say that the code added
before the return instructions may require an operand stack of size up to s + 4.
This worst case scenario rarely happens in practice: with common compilers
the operand stack before a RETURN contains only the return value, i.e. it has
a size of 0, 1 or 2 at most. But if we want to handle all possible cases, we
need to use the worst case scenario2 . We must then override the visitMaxs
method as follows:
public void visitMaxs(int maxStack, int maxLocals) {
mv.visitMaxs(maxStack + 4, maxLocals);
}
Of course it is possible to not bother about the maximum stack size and rely
on the COMPUTE_MAXS option that, in addition, will compute the optimal value
and not a worst case value. But for such simple transformations it does not
cost much effort to update maxStack manually.
2
hopefully it is not necessary to give the optimal operand stack size. Giving any value
greater than or equal to this optimal value is possible, although it may waste memory on
the thread’s execution stack.
50
3.2. Interfaces and components
Now an interesting question is: what about stack map frames? The original
code did not contain any frame, nor the transformed one, but is this due to the
specific code we used as example? are there some situations where frames must
be updated? The answer is no because 1) the inserted code leaves the operand
stack unchanged, 2) the inserted code does not contain jump instructions and
3) the jump instructions – or, more formally, the control flow graph – of the
original code is not modified. This means that the original frames do not
change, and since no new frames must be stored for the inserted code, the
compressed original frames do not change either.
We can now put all the elements together in associated ClassVisitor and
MethodVisitor subclasses:
public class AddTimerAdapter extends ClassVisitor {
private String owner;
private boolean isInterface;
public AddTimerAdapter(ClassVisitor cv) {
super(ASM4, cv);
}
@Override public void visit(int version, int access, String name,
String signature, String superName, String[] interfaces) {
cv.visit(version, access, name, signature, superName, interfaces);
owner = name;
isInterface = (access & ACC_INTERFACE) != 0;
}
@Override public MethodVisitor visitMethod(int access, String name,
String desc, String signature, String[] exceptions) {
MethodVisitor mv = cv.visitMethod(access, name, desc, signature,
exceptions);
if (!isInterface && mv != null && !name.equals("<init>")) {
mv = new AddTimerMethodAdapter(mv);
}
return mv;
}
@Override public void visitEnd() {
if (!isInterface) {
FieldVisitor fv = cv.visitField(ACC_PUBLIC + ACC_STATIC, "timer",
"J", null, null);
if (fv != null) {
fv.visitEnd();
}
}
cv.visitEnd();
}
class AddTimerMethodAdapter extends MethodVisitor {
public AddTimerMethodAdapter(MethodVisitor mv) {
super(ASM4, mv);
51
3. Methods
}
@Override public void visitCode() {
mv.visitCode();
mv.visitFieldInsn(GETSTATIC, owner, "timer", "J");
mv.visitMethodInsn(INVOKESTATIC, "java/lang/System",
"currentTimeMillis", "()J");
mv.visitInsn(LSUB);
mv.visitFieldInsn(PUTSTATIC, owner, "timer", "J");
}
@Override public void visitInsn(int opcode) {
if ((opcode >= IRETURN && opcode <= RETURN) || opcode == ATHROW) {
mv.visitFieldInsn(GETSTATIC, owner, "timer", "J");
mv.visitMethodInsn(INVOKESTATIC, "java/lang/System",
"currentTimeMillis", "()J");
mv.visitInsn(LADD);
mv.visitFieldInsn(PUTSTATIC, owner, "timer", "J");
}
mv.visitInsn(opcode);
}
@Override public void visitMaxs(int maxStack, int maxLocals) {
mv.visitMaxs(maxStack + 4, maxLocals);
}
}
}
The class adapter is used to instantiate the method adapter (except for con-
structors), but also to add the timer field and to store the name of the class
that is being transformed in a field that can be accessed from the method
adapter.
The transformation seen in the previous section is local and does not depend
on the instructions that have been visited before the current one: the code
added at the beginning is always the same and is always added, and likewise
for the code inserted before each RETURN instruction. Such transformations
are called stateless transformations. They are simple to implement but only
the simplest transformations verify this property.
More complex transformations require memorizing some state about the in-
structions that have been visited before the current one. Consider for example
a transformation that removes all occurrences of the ICONST_0 IADD sequence,
whose empty effect is to add 0. It is clear that when an IADD instruction is vis-
ited, it must be removed only if the last visited instruction was an ICONST_0.
52
3.2. Interfaces and components
This requires storing state inside the method adapter. For this reason such
transformations are called statefull transformations.
Let’s look in more details at this example. When an ICONST_0 is visited,
it must be removed only if the next instruction is an IADD. The problem is
that the next instruction is not yet known. The solution is to postpone this
decision to the next instruction: if it is an IADD then remove both instructions,
otherwise emit the ICONST_0 and the current instruction.
In order to implement transformations that remove or replace some instruc-
tion sequence, it is convenient to introduce a MethodVisitor subclass whose
visitXxx Insn methods call a common visitInsn() method:
public abstract class PatternMethodAdapter extends MethodVisitor {
protected final static int SEEN_NOTHING = 0;
protected int state;
public PatternMethodAdapter(int api, MethodVisitor mv) {
super(api, mv);
}
@Overrid public void visitInsn(int opcode) {
visitInsn();
mv.visitInsn(opcode);
}
@Override public void visitIntInsn(int opcode, int operand) {
visitInsn();
mv.visitIntInsn(opcode, operand);
}
...
protected abstract void visitInsn();
}
53
3. Methods
return;
}
mv.visitInsn(opcode);
}
@Override protected void visitInsn() {
if (state == SEEN_ICONST_0) {
mv.visitInsn(ICONST_0);
}
state = SEEN_NOTHING;
}
}
The visitInsn(int) method first tests if the sequence has been detected. In
this case it reinitializes state and returns immediately, which has the effect
of removing the sequence. In the other cases it calls the common visitInsn
method, which emits an ICONST_0 if this was the last visited instruction.
Then, if the current instruction is an ICONST_0, it memorizes this fact and
returns, in order to postpone the decision about this instruction. In all other
cases the current instruction is forwarded to the next visitor.
As we have seen in the previous sections, labels and frames are visited just
before their associated instruction. In other words they are visited at the same
time as instructions, although they are not instructions themselves. This has
an impact on transformations that detect instruction sequences, but this im-
pact is in fact an advantage. Indeed, what happens if one of the instructions
we remove is the target of a jump instruction? If some instruction may jump
to the ICONST_0, this means that there is a label designating this instruc-
tion. After the removal of the two instructions this label will designate the
instruction that follows the removed IADD, which is what we want. But if
some instruction may jump to the IADD, we can not remove the instruction
sequence (we can not be sure that before this jump a 0 was pushed on the
stack). Hopefully, in this case, there must be a label between the ICONST_0
and the IADD, which can easily be detected.
The reasoning is the same for stack map frames: if a stack map frame is
visited between the two instructions, we can not remove them. Both cases
can be handled by considering labels and frames as instructions in the pattern
matching algorithm. This can be done in PatternMethodAdapter (note that
visitMaxs also calls the common visitInsn method; this is used to handle
the case where the end of the method is a prefix of the sequence that must be
detected):
54
3.2. Interfaces and components
As we will see in the next chapter, a compiled method may contain information
about source file line numbers, used for instance in exception stack traces. This
information is visited with the visitLineNumber method, which is also called
at the same time as instructions. Here however the presence of line numbers
in the middle of an instruction sequence does not have any impact on the
possibility to transform or remove it. The solution is therefore to ignore them
completely in the pattern matching algorithm.
Each transition is labeled with a condition (the value of the current instruc-
tion) and an action (an instruction sequence that must be emitted, in bold).
For instance the transition from S1 to S0 happens if the current instruction
is not an ALOAD 0. In this case the ALOAD 0 that was visited to arrive at this
state is emitted. Note the transition from S2 to itself: this happens when
three or more consecutive ALOAD 0 are found. In this case we stay in the
state where two ALOAD 0 have been visited, and we emit the third one. Once
the state machine has been found, writing the corresponding method adapter
55
3. Methods
56
3.2. Interfaces and components
break;
case SEEN_ALOAD_0ALOAD_0: // S2 -> S2
if (opcode == ALOAD && var == 0) {
mv.visitVarInsn(ALOAD, 0);
return;
}
break;
}
visitInsn();
mv.visitVarInsn(opcode, var);
}
@Override
public void visitFieldInsn(int opcode, String owner, String name,
String desc) {
switch (state) {
case SEEN_ALOAD_0ALOAD_0: // S2 -> S3
if (opcode == GETFIELD) {
state = SEEN_ALOAD_0ALOAD_0GETFIELD;
fieldOwner = owner;
fieldName = name;
fieldDesc = desc;
return;
}
break;
case SEEN_ALOAD_0ALOAD_0GETFIELD: // S3 -> S0
if (opcode == PUTFIELD && name.equals(fieldName)) {
state = SEEN_NOTHING;
return;
}
break;
}
visitInsn();
mv.visitFieldInsn(opcode, owner, name, desc);
}
@Override protected void visitInsn() {
switch (state) {
case SEEN_ALOAD_0: // S1 -> S0
mv.visitVarInsn(ALOAD, 0);
break;
case SEEN_ALOAD_0ALOAD_0: // S2 -> S0
mv.visitVarInsn(ALOAD, 0);
mv.visitVarInsn(ALOAD, 0);
break;
case SEEN_ALOAD_0ALOAD_0GETFIELD: // S3 -> S0
mv.visitVarInsn(ALOAD, 0);
mv.visitVarInsn(ALOAD, 0);
mv.visitFieldInsn(GETFIELD, fieldOwner, fieldName, fieldDesc);
57
3. Methods
break;
}
state = SEEN_NOTHING;
}
}
Note that, for the same reasons as in the AddTimerAdapter case in section
3.2.4, the statefull transformations presented in this section do not need to
transform stack map frames: the original frames stay valid after the transfor-
mation. They don’t even need to transform the local variables and operand
stack size. Finally it must be noted that statefull transformations are not
limited to transformations that detect and transform instruction sequences.
Many other types of transformation are also statefull. This is the case, for
instance, of the method adapters presented in the next section.
3.3. Tools
The tools presented in section 2.3 can also be used for methods.
Type
58
3.3. Tools
TraceClassVisitor
This class, which has already been presented in the previous chapter, prints a
textual representation of the classes it visits, including a textual representation
of their methods, in a form very similar to the one used in this chapter. It can
therefore be used to trace the content of generated or transformed methods at
any point in a transformation chain. For example:
java -classpath asm.jar:asm-util.jar \
org.objectweb.asm.util.TraceClassVisitor \
java.lang.Void
prints:
// class version 49.0 (49)
// access flags 49
public final class java/lang/Void {
// access flags 25
// signature Ljava/lang/Class<Ljava/lang/Void;>;
// declaration: java.lang.Class<java.lang.Void>
public final static Ljava/lang/Class; TYPE
// access flags 2
private <init>()V
ALOAD 0
INVOKESPECIAL java/lang/Object.<init> ()V
RETURN
MAXSTACK = 1
MAXLOCALS = 1
// access flags 8
static <clinit>()V
LDC "void"
INVOKESTATIC java/lang/Class.getPrimitiveClass (...)...
PUTSTATIC java/lang/Void.TYPE : Ljava/lang/Class;
RETURN
MAXSTACK = 1
MAXLOCALS = 0
}
This shows how to generate a static block static { ... }, namely with
a <clinit> method (for CLass INITializer). Note that if you want to trace
the content of a single method at some point in a chain, instead of trac-
ing all the content of its class, you can use TraceMethodVisitor instead of
TraceClassVisitor (in this case you must specify the backend explicitly; here
we use a Textifier):
public MethodVisitor visitMethod(int access, String name,
String desc, String signature, String[] exceptions) {
MethodVisitor mv = cv.visitMethod(access, name, desc, signature,
exceptions);
59
3. Methods
CheckClassAdapter
This class, which has already been presented in the previous chapter, checks
that the ClassVisitor methods are called in the appropriate order, and with
valid arguments, and it does the same for the MethodVisitor methods. It
can therefore be used to check that the MethodVisitor API is correctly used
at any point in a transformation chain. Like with TraceMethodVisitor, you
can use the CheckMethodAdapter class to check a single method instead of
checking all its class:
public MethodVisitor visitMethod(int access, String name,
String desc, String signature, String[] exceptions) {
MethodVisitor mv = cv.visitMethod(access, name, desc, signature,
exceptions);
if (debug && mv != null && ...) { // if this method must be checked
mv = new CheckMethodAdapter(mv);
}
return new MyMethodAdapter (mv);
}
This code checks that MyMethodAdapter uses the MethodVisitor API cor-
rectly. Note however that this adapter will not check that the bytecode is
correct: for instance it will not detect that ISTORE 1 ALOAD 1 is invalid. In
fact this kind of error can be detected, if you use the other constructor of
CheckMethodAdapter (see the Javadoc), and if you provide valid maxStack
and maxLocals arguments in visitMaxs.
ASMifier
This class, which has already been presented in the previous chapter, also
works with the content of methods. It can be used to know how to generate
60
3.3. Tools
some compiled code with ASM: just write the corresponding source code in
Java, compile it with javac, and use the ASMifier to visit this class. You
will get the ASM code to generate the bytecode corresponding to your source
code.
3.3.2. AnalyzerAdapter
This method adapter computes the stack map frames before each instruction,
based on the frames visited in visitFrame. Indeed, as explained in section
3.1.5, visitFrame is only called before some specific instructions in a method,
in order to save space, and because “the other frames can be easily and quickly
inferred from these ones”. This is what this adapter does. Of course it only
works on classes that contain precomputed stack map frames, i.e. compiled
with Java 6 or higher (or previously upgraded to Java 6 with an ASM adapter
using the COMPUTE_FRAMES option).
In the case of our AddTimerAdapter example, this adapter could be used to
get the size of the operand stack just before the RETURN instructions, thereby
allowing to compute an optimal transformed value for maxStack in visitMaxs
(in fact this method is not recommended in practice, because it is much less
efficient than using COMPUTE_MAXS):
class AddTimerMethodAdapter2 extends AnalyzerAdapter {
private int maxStack;
public AddTimerMethodAdapter2(String owner, int access,
String name, String desc, MethodVisitor mv) {
super(ASM4, owner, access, name, desc, mv);
}
@Override public void visitCode() {
super.visitCode();
mv.visitFieldInsn(GETSTATIC, owner, "timer", "J");
mv.visitMethodInsn(INVOKESTATIC, "java/lang/System",
"currentTimeMillis", "()J");
mv.visitInsn(LSUB);
mv.visitFieldInsn(PUTSTATIC, owner, "timer", "J");
maxStack = 4;
}
@Override public void visitInsn(int opcode) {
if ((opcode >= IRETURN && opcode <= RETURN) || opcode == ATHROW) {
mv.visitFieldInsn(GETSTATIC, owner, "timer", "J");
mv.visitMethodInsn(INVOKESTATIC, "java/lang/System",
"currentTimeMillis", "()J");
mv.visitInsn(LADD);
mv.visitFieldInsn(PUTSTATIC, owner, "timer", "J");
61
3. Methods
The stack field is defined in the AnalyzerAdapter class, and contains the
types in operand stack. More precisely, in a visitXxx Insn, and before the
overridden method is called, this list contains the state of the operand stack
just before this instruction. Note that the overridden methods must be called
so that the stack field is correctly updated (hence the use of super instead
of mv in the original code).
Alternatively the new instructions can be inserted by calling the methods of
the super class: the effect is that the frames for these instructions will be
computed by AnalyzerAdapter. Since, in addition, this adapter updates the
arguments of visitMaxs based on the frames it computes, we do not need to
update them ourselves:
62
3.3. Tools
3.3.3. LocalVariablesSorter
This method adapter renumbers the local variables used in a method in the
order they appear in this method. For instance in a method with two param-
eters, the first local variable read or written whose index is greater than or
equal to 3 – the first three local variables correspond to this and to the two
method parameters, and can therefore not be changed – is assigned index 3,
the second one is assigned index 4, and so on. This adapter is useful to insert
new local variables in a method. Without this adapter it would be necessary
to add new local variables after all the existing ones, but unfortunately their
number is not known until the end of the method, in visitMaxs.
In order to show how this adapter can be used, let’s suppose that we want to
use a local variable to implement AddTimerAdapter:
public class C {
public static long timer;
public void m() throws Exception {
long t = System.currentTimeMillis();
Thread.sleep(100);
timer += System.currentTimeMillis() - t;
}
}
63
3. Methods
mv.visitInsn(LADD);
mv.visitFieldInsn(PUTSTATIC, owner, "timer", "J");
}
super.visitInsn(opcode);
}
@Override public void visitMaxs(int maxStack, int maxLocals) {
super.visitMaxs(maxStack + 4, maxLocals);
}
}
Note that the original frames associated to the method become invalid when
the local variables are renumbered, and a fortiori when new local variables
are inserted. Hopefully it is possible to avoid recomputing these frames from
scratch: indeed no frames must be added or removed, and it “suffices” to re-
order the content of local variables in the original frames to get the frames of
the transformed method. LocalVariablesSorter takes care of that automat-
ically. If you also need to do incremental stack map frame updates for one of
your method adapters, you can inspire yourself from the source of this class.
As you can see above using a local variable does not solve the problem we
had in the original version of this class, concerning the worst case value for
maxStack. If you want to use an AnalyzerAdapter to solve that, in addition
to a LocalVariablesSorter, you must use these adapters through delegation
instead of via inheritance (since multiple inheritance is not possible):
class AddTimerMethodAdapter5 extends MethodVisitor {
public LocalVariablesSorter lvs;
public AnalyzerAdapter aa;
private int time;
private int maxStack;
public AddTimerMethodAdapter5(MethodVisitor mv) {
super(ASM4, mv);
}
@Override public void visitCode() {
mv.visitCode();
mv.visitMethodInsn(INVOKESTATIC, "java/lang/System",
"currentTimeMillis", "()J");
time = lvs.newLocal(Type.LONG_TYPE);
mv.visitVarInsn(LSTORE, time);
maxStack = 4;
}
@Override public void visitInsn(int opcode) {
if ((opcode >= IRETURN && opcode <= RETURN) || opcode == ATHROW) {
mv.visitMethodInsn(INVOKESTATIC, "java/lang/System",
"currentTimeMillis", "()J");
mv.visitVarInsn(LLOAD, time);
mv.visitInsn(LSUB);
64
3.3. Tools
3.3.4. AdviceAdapter
This method adapter is an abstract class that can be used to insert code at
the beginning of a method and just before any RETURN or ATHROW instruction.
Its main advantage is that it also works for constructors, where code must not
be inserted just at the beginning of the constructor, but after the call to the
super constructor. In fact most of the code of this adapter is dedicated to the
detection of this super constructor call.
If you look carefully at the AddTimerAdapter class in section 3.2.4, you will
see that the AddTimerMethodAdapter is not used for constructors, because of
this problem. By inheriting from AdviceAdapter this method adapter can be
improved to work on constructors too (note that AdviceAdapter inherits from
LocalVariablesSorter, so we could also easily use a local variable):
class AddTimerMethodAdapter6 extends AdviceAdapter {
public AddTimerMethodAdapter6(int access, String name, String desc,
MethodVisitor mv) {
super(ASM4, mv, access, name, desc);
65
3. Methods
}
@Override protected void onMethodEnter() {
mv.visitFieldInsn(GETSTATIC, owner, "timer", "J");
mv.visitMethodInsn(INVOKESTATIC, "java/lang/System",
"currentTimeMillis", "()J");
mv.visitInsn(LSUB);
mv.visitFieldInsn(PUTSTATIC, owner, "timer", "J");
}
@Override protected void onMethodExit(int opcode) {
mv.visitFieldInsn(GETSTATIC, owner, "timer", "J");
mv.visitMethodInsn(INVOKESTATIC, "java/lang/System",
"currentTimeMillis", "()J");
mv.visitInsn(LADD);
mv.visitFieldInsn(PUTSTATIC, owner, "timer", "J");
}
@Override public void visitMaxs(int maxStack, int maxLocals) {
super.visitMaxs(maxStack + 4, maxLocals);
}
}
66
4. Metadata
This chapter explains how to generate and transform compiled Java classes
metadata, such as annotations, with the core API. Each section starts with
a presentation of one type of metadata, and then presents the correspond-
ing ASM interfaces, components and tools to generate and transform these
metadata, with some illustrative examples.
4.1. Generics
Generic classes such as List<E>, and classes using them, contain information
about the generic types they declare or use. This information is not used at
runtime by the bytecode instructions, but it can be accessed via the reflection
API. It is also used by compilers, for separate compilation.
4.1.1. Structure
For backward compatibility reasons the information about generic types is not
stored in type or method descriptors (which were defined long before the in-
troduction of generics in Java 5), but in similar constructs called type, method
and class signatures. These signatures are stored in addition to descriptors in
class, field and method declarations when a generic type is involved (generic
types do not affect the bytecode of methods: the compiler uses them to per-
form static type checks, but then compiles methods as if they were not used,
by reintroducing type casts where necessary).
Unlike type and method descriptors, and due to the recursive nature of generic
types (a generic type can be parameterized by a generic type – consider for
example List<List<E>>) the grammar of type signatures is quite complex. It
is given by the following rules (see the Java Virtual Machine Specification for
a complete description of these rules):
67
4. Metadata
TypeSignature: Z | C | B | S | I | F | J | D | FieldTypeSignature
FieldTypeSignature: ClassTypeSignature | [ TypeSignature | TypeVar
ClassTypeSignature: L Id ( / Id )* TypeArgs? ( . Id TypeArgs? )* ;
TypeArgs: < TypeArg+ >
TypeArg: * | ( + | - )? FieldTypeSignature
TypeVar: T Id ;
The first rule says that a type signature is either a primitive type descriptor
or a field type signature. The second rule defines a field type signature as a
class type signature, an array type signature, or a type variable. The third rule
defines class type signatures: they are class type descriptors with possible type
arguments, between angle brackets, after the main class name or after the inner
class names (prefixed with dots). The remaining rules define type arguments
and type variables. Note that a type argument can be a complete field type
signature, with its own type arguments: type signatures can therefore be very
complex (see Figure 4.1).
Method signatures extend method descriptors like type signatures extend type
descriptors. A method signature describes the type signatures of the method
parameters and the signature of its return type. Unlike method descriptors, it
also contains the signatures of the exceptions thrown by the method, preceded
by ^, and can also contain optional formal type parameters between angle
brackets:
68
4.1. Generics
MethodTypeSignature:
TypeParams? ( TypeSignature* ) ( TypeSignature | V ) Exception*
For example the method signature of the following generic static method,
parameterized by the type variable T:
<T:Ljava/lang/Object;>(I)Ljava/lang/Class<+TT;>;
Finally a class signature, which must not be confused with a class type sig-
nature, is defined as the type signature of its super class, followed by the
type signatures of the implemented interfaces, and with optional formal type
parameters:
For example the class signature of a class declared as C<E> extends List<E>
is <E:Ljava/lang/Object;>Ljava/util/List<TE;>;.
Like for descriptors, and for the same efficiency reasons (see section 2.3.1), the
ASM API exposes signatures as they as stored in compiled classes (the main
occurences of signatures are in the visit, visitField and visitMethod meth-
ods of the ClassVisitor class, as an optional class, type or method signature
argument respectively). Hopefully it also provides some tools to generate and
transform signatures, in the org.objectweb.asm.signature package, based
on the SignatureVisitor abstract class (see Figure 4.2).
69
4. Metadata
This abstract class is used to visit type signatures, method signatures and
class signatures. The methods used to visit type signatures are in bold, and
must be called in the following order, which reflects the previous grammar
rules (note that two of them return a SignatureVisitor: this is due to the
recursive definition of type signatures):
visitBaseType | visitArrayType | visitTypeVariable |
( visitClassType visitTypeArgument*
( visitInnerClassType visitTypeArgument* )* visitEnd ) )
70
4.1. Generics
71
4. Metadata
}
public void visitTypeArgument() {
sv.visitTypeArgument();
}
public SignatureVisitor visitTypeArgument(char wildcard) {
sv.visitTypeArgument(wildcard);
return this;
}
public void visitEnd() {
sv.visitEnd();
}
}
String s = "Ljava/util/HashMap<TK;TV;>.HashIterator<TK;>;";
Map<String, String> renaming = new HashMap<String, String>();
renaming.put("java/util/HashMap", "A");
renaming.put("java/util/HashMap.HashIterator", "B");
SignatureWriter sw = new SignatureWriter();
SignatureVisitor sa = new RenameSignatureAdapter(sw, renaming);
SignatureReader sr = new SignatureReader(s);
sr.acceptType(sa);
sw.toString();
4.1.3. Tools
4.2. Annotations
72
4.2. Annotations
4.2.1. Structure
The ASM API for generating and transforming annotations is based on the
AnnotationVisitor abstract class (see Figure 4.3).
public abstract class AnnotationVisitor {
public AnnotationVisitor(int api);
public AnnotationVisitor(int api, AnnotationVisitor av);
public void visit(String name, Object value);
public void visitEnum(String name, String desc, String value);
public AnnotationVisitor visitAnnotation(String name, String desc);
public AnnotationVisitor visitArray(String name);
public void visitEnd();
}
The methods of this class are used to visit the name value pairs of an anno-
tation (the annnotation type is visited in the methods that return this type,
i.e. the visitAnnotation methods). The first method is used for primitive,
String and Class values (the later being represented by Type objects), and
the others are used for enum, annotation and array values. They can be called
in any order, except visitEnd:
( visit | visitEnum | visitAnnotation | visitArray )* visitEnd
73
4. Metadata
Like for fields and methods, an annotation can be removed by returning null
in the visitAnnotation methods:
public class RemoveAnnotationAdapter extends ClassVisitor {
private String annDesc;
public RemoveAnnotationAdapter(ClassVisitor cv, String annDesc) {
super(ASM4, cv);
this.annDesc = annDesc;
}
@Override
public AnnotationVisitor visitAnnotation(String desc, boolean vis) {
if (desc.equals(annDesc)) {
return null;
}
return cv.visitAnnotation(desc, vis);
}
}
74
4.2. Annotations
Note that this adapter upgrades the class version to 1.5 if it was less than
that. This is necessary because the JVM ignores annotations in classes whose
version is less than 1.5.
75
4. Metadata
The last and probably most frequent use case of annotations in class and
method adapters is to use annotations in order to parameterize a transfor-
mation. For instance you could transform field accesses only for fields that
have a @Persistent annotation, add logging code only to methods that have
a @Log annotation, and so on. All these use cases can easily be implemented
because annotations must be visited first: class annotations must be visited
before fields and methods, and method and parameter annotations must be
visited before the code. It is therefore sufficient to set a flag when the desired
annotation is detected, and to use it later on in the transformation, as is done
in the above example with the isAnnotationPresent flag.
4.2.3. Tools
The TraceClassVisitor, CheckClassAdapter and ASMifier classes, pre-
sented in section 2.3, also support annotations (like for methods, it is also pos-
sible to use TraceAnnotationVisitor or CheckAnnotationAdapter to work
at the level of individual annotations instead of at the class level). They can
be used to see how to generate some specific annotation. For example using:
java -classpath asm.jar:asm-util.jar \
org.objectweb.asm.util.ASMifier \
java.lang.Deprecated
76
4.3. Debug
}
cw.visitEnd();
return cw.toByteArray();
}
}
This code shows how two create an annotation class, with the ACC_ANNOTATION
flag, and shows how to create two class annotations, one without value, and
one with an enum value. Method and parameter annotations can be created
in a similar way, with the visitAnnotation and visitParameterAnnotation
methods defined in the MethodVisitor class.
4.3. Debug
Classes compiled with javac -g contain the name of their source file, a map-
ping between source line numbers and bytecode instructions, and a mapping
betwen local variable names in source code and local variable slots in bytecode.
This optional information is used in debuggers and in exception stack traces
when it is available.
4.3.1. Structure
The source file name of a class is stored in a dedicated class file structure
section (see Figure 2.1).
The mapping between source line numbers and bytecode instructions is stored
as a list of (line number, label) pairs in the compiled code section of methods.
For example if l1 , l2 and l3 are three labels that appear in this order, then
the following pairs:
(n1, l1)
(n2, l2)
(n3, l3)
mean that instructions between l1 and l2 come from line n1, that instructions
between l2 and l3 come from line n2, and that instructions after l3 come
from line n3. Note that a given line number can appear in several pairs.
This is because the instructions corresponding to expressions that appear on
a single source line may not be contiguous in the bytecode. For example for
(init; cond; incr) statement; is generaly compiled in the following order:
init statement incr cond.
77
4. Metadata
The mapping between local variable names in source code and local variable
slots in bytecode is stored as a list of (name, type descriptor, type signature,
start, end, index) tuples in the compiled code section of methods. Such a tuple
means that, between the two labels start and end, the local variable in slot
index corresponds to the local variable whose name and type in source code
are given by the first three tuple elements. Note that the compiler may use the
same local variable slot to store distinct source local variables with different
scopes. Conversely a unique source local variable may be compiled into a local
variable slot with a non contiguous scope. For instance it is possible to have
a situation like this:
l1:
... // here slot 1 contains local variable i
l2:
... // here slot 1 contains local variable j
l3:
... // here slot 1 contains local variable i again
end :
The debug information is visited with three methods of the ClassVisitor and
MethodVisitor classes:
• the source file name is visited with the visitSource method of the
ClassVisitor class;
• the mapping between source line numbers and bytecode instructions is
visited with the visitLineNumber method of the MethodVisitor class,
one pair at a time;
• the mapping between local variable names in source code and local vari-
able slots in bytecode is visited with the visitLocalVariable method
of the MethodVisitor class, one tuple at a time.
The visitLineNumber method must be called after the label passed as argu-
ment has been visited. In practice it is called just after this label, which makes
it very easy to know the source line of the current instruction in a method
visitor:
78
4.3. Debug
In order to visit line numbers and local variable names, the ClassReader class
may need to introduce “artificial” Label objects, in the sense that they are
not needed by jump instructions, but only to represent the debug information.
This can introduce false positives in situations such as the one explained in
section 3.2.5, where a Label in the middle of an instruction sequence was
considered to be a jump target, and therefore prevented this sequence from
being removed.
In order to avoid these false positives it is possible to use the SKIP_DEBUG
option in the ClassReader.accept method. With this option the class reader
does not visit the debug information, and does not create artificial labels for
it. Of course the debug information will be removed from the class, so this
option can be used only if this is not a problem for your application.
Note: the ClassReader class provides other options such as SKIP_CODE to
skip the visit of compiled code (this can be useful if you just need
the class structure), SKIP_FRAMES to skip the stack map frames, and
EXPAND_FRAMES to uncompress these frames.
79
4. Metadata
4.3.3. Tools
Like for generic types and annotations, you can use the TraceClassVisitor,
CheckClassAdapter and ASMifier classes to find how to work with debug
information.
80
5. Backward compatibility
5.1. Introduction
New elements have been introduced in the past in the class file format, and
new elements will continue to be added in the future (e.g., for modularity,
annotations on Java types, etc). Up to ASM 3.x, each such change led to
backward incompatible changes in the ASM API, which is not good. To solve
these problems, a new mechanism has been introduced in ASM 4.0. Its goal
is to ensure that all future ASM versions will remain backward compatible
with any previous version, down to ASM 4.0, even when new features will
be introduced to the class file format. This means that a class generator, a
class analyzer or a class adapter written for one ASM version, starting from
4.0, will still be usable with any future ASM version. However, this property
can not be ensured by ASM alone. It requires users to follow a few simple
guidelines when writing their code. The goal of this chapter is to present these
guidelines, and to give an idea of the internal mechanism used in the ASM
core API to ensure backward compatibility.
81
5. Backward compatibility
82
5.1. Introduction
All this leads to the definition of the following backward compatibility con-
tract:
• ASM version X is written for Java classes whose version is less than or
equal to x. It cannot generate classes with a version y > x, and it must
fail if given as input, in ClassReader.accept, a class whose version is
greater than x.
• code written for ASM X and following the guidelines presented below
must continue to work, unmodified, with input classes up to version x,
with any future version Y > X of ASM.
• code written for ASM X and following the guidelines presented below
must continue to work, unmodified, with input classes whose declared
version is y but that only use features defined in versions older or equal
to x, with ASM Y or any future version.
• code written for ASM X and following the guidelines presented below
must fail if given as input a class that uses features introduced in class
versions y > x, with ASM X or any other future version.
Note that the last three points do not concern class generators, which do not
have class inputs.
5.1.2. An example
In order to illustrate the user guidelines and the internal ASM mechanism
ensuring backward compatibility, we suppose in this chapter that two new
imaginary attributes will be added to Java 8 classes, one to store the class au-
thor(s), and one to store its license. We also suppose that these new attributes
will be exposed via two new methods in ClassVisitor, in ASM 5.0:
void visitLicense(String license);
to visit the license, and a new version of visitSource to visit the author at
the same time as the source file name and debug information1 :
void visitSource(String author, String source, String debug);
83
5. Backward compatibility
The author and license attributes are optional, i.e., calling visitLicense is
not mandatory, and author can be null in a visitSource call.
5.2. Guidelines
This section presents the guidelines that you must follow when using the core
ASM API, in order to ensure that your code will remain valid with any future
ASM versions (in the sense of the above contract).
First of all, if you write a class generator, you don’t have any guideline to fol-
low. For example, if you write a class generator for ASM 4.0, it will probably
contain a call like visitSource(mySource, myDebug), and of course no call
to visitLicense. If you run it unchanged with ASM 5.0, this will call the
deprecated visitSource method, but the ASM 5.0 ClassWriter will inter-
nally redirect this to visitSource(null, mySource, myDebug), yielding the
expected result (but a bit less efficiently than if you upgrade your code to call
the new method directly). Likewise, the absence of a call to visitLicense
will not be a problem (the generated class version will not have changed either,
and classes of this version are not expected to have a license attribute).
If, on the other hand, you write a class analyzer or a class adapter, i.e. if you
override the ClassVisitor class (or any other similar class like FieldVisitor
or MethodVisitor), you must follow a few guidelines, presented below.
84
5.2. Guidelines
85
5. Backward compatibility
...
public void visitSource(String source, String debug) {
if (api < ASM5) {
if (cv != null) cv.visitSource(source, debug);
} else {
visitSource(null, source, debug);
}
}
public void visitSource(Sring author, String source, String debug) {
if (api < ASM5) {
if (author == null) {
visitSource(source, debug);
} else {
throw new RuntimeException();
}
} else {
if (cv != null) cv.visitSource(author, source, debug);
}
}
public void visitLicense(String license) {
if (api < ASM5) throw new RuntimeException();
if (cv != null) cv.visitSource(source, debug);
}
}
86
5.2. Guidelines
87
5. Backward compatibility
88
Part II.
Tree API
89
6. Classes
This chapter explains how to generate and transform classes with the ASM
tree API. It starts with a presentation of the tree API alone, and then explains
how to compose it with the core API. The tree API for the content of methods,
annotations and generics is explained in the next chapters.
6.1.1. Presentation
The ASM tree API for generating and transforming compiled Java classes is
based on the ClassNode class (see Figure 6.1).
public class ClassNode ... {
public int version;
public int access;
public String name;
public String signature;
public String superName;
public List<String> interfaces;
public String sourceFile;
public String sourceDebug;
public String outerClass;
public String outerMethod;
public String outerMethodDesc;
public List<AnnotationNode> visibleAnnotations;
public List<AnnotationNode> invisibleAnnotations;
public List<Attribute> attrs;
public List<InnerClassNode> innerClasses;
public List<FieldNode> fields;
public List<MethodNode> methods;
}
91
6. Classes
As you can see the public fields of this class correspond to the class file struc-
ture sections presented in Figure 2.1. The content of these fields is the same
as in the core API. For instance name is an internal name and signature is a
class signature (see sections 2.1.2 and 4.1). Some fields contain other Xxx Node
classes: these classes, presented in details in the next chapters, have a simi-
lar structure, i.e. have fields that correspond to sub sections of the class file
structure. For instance the FieldNode class looks like this:
public class FieldNode ... {
public int access;
public String name;
public String desc;
public String signature;
public Object value;
public FieldNode(int access, String name, String desc,
String signature, Object value) {
...
}
...
}
Generating a class with the tree API simply consists in creating a ClassNode
object and in initializing its fields. For instance the Comparable interface in
section 2.2.3 can be built as follows, with approximatively the same amount
of code as in section 2.2.3:
ClassNode cn = new ClassNode();
cn.version = V1_5;
cn.access = ACC_PUBLIC + ACC_ABSTRACT + ACC_INTERFACE;
92
6.1. Interfaces and components
cn.name = "pkg/Comparable";
cn.superName = "java/lang/Object";
cn.interfaces.add("pkg/Mesurable");
cn.fields.add(new FieldNode(ACC_PUBLIC + ACC_FINAL + ACC_STATIC,
"LESS", "I", null, new Integer(-1)));
cn.fields.add(new FieldNode(ACC_PUBLIC + ACC_FINAL + ACC_STATIC,
"EQUAL", "I", null, new Integer(0)));
cn.fields.add(new FieldNode(ACC_PUBLIC + ACC_FINAL + ACC_STATIC,
"GREATER", "I", null, new Integer(1)));
cn.methods.add(new MethodNode(ACC_PUBLIC + ACC_ABSTRACT,
"compareTo", "(Ljava/lang/Object;)I", null, null));
Using the tree API to generate a class takes about 30% more time (see Ap-
pendix A.1) and consumes more memory than using the core API. But it
makes it possible to generate the class elements in any order, which can be
convenient in some cases.
93
6. Classes
}
@Override public void transform(ClassNode cn) {
Iterator<MethodNode> i = cn.methods.iterator();
while (i.hasNext()) {
MethodNode mn = i.next();
if (methodName.equals(mn.name) && methodDesc.equals(mn.desc)) {
i.remove();
}
}
super.transform(cn);
}
}
As can be seen the main difference with the core API is that you need to
iterate over all methods, while you don’t need to do so with the core API (this
is done for you in ClassReader). In fact this difference is valid for almost
all tree based transformations. For instance the AddFieldAdapter of section
2.2.6 also needs an iterator when implemented with the tree API:
public class AddFieldTransformer extends ClassTransformer {
private int fieldAccess;
private String fieldName;
private String fieldDesc;
public AddFieldTransformer(ClassTransformer ct, int fieldAccess,
String fieldName, String fieldDesc) {
super(ct);
this.fieldAccess = fieldAccess;
this.fieldName = fieldName;
this.fieldDesc = fieldDesc;
}
@Override public void transform(ClassNode cn) {
boolean isPresent = false;
for (FieldNode fn : cn.fields) {
if (fieldName.equals(fn.name)) {
isPresent = true;
break;
}
}
if (!isPresent) {
cn.fields.add(new FieldNode(fieldAccess, fieldName, fieldDesc,
null, null));
}
super.transform(cn);
}
}
Like for class generation, using the tree API to transform classes takes more
time and consumes more memory than using the core API. But it makes it
94
6.2. Components composition
possible to implement some transformations more easily. This is the case, for
example, of a transformation that adds to a class an annotation containing a
digital signature of its content. With the core API the digital signature can
be computed only when all the class has been visited, but then it is too late to
add an annotation containing it, because annotations must be visited before
class members. With the tree API this problem disappears because there is
no such constraint in this case.
In fact it is possible to implement the AddDigitialSignature example with
the core API, but then the class must be transformed in two passes. During
the first pass the class is visited with a ClassReader (and no ClassWriter),
in order to compute the digital signature based on the class content. During
the second pass the same ClassReader is reused to do a second visit of the
class, this time with an AddAnnotationAdapter chained to a ClassWriter.
By generalizing this argument we see that, in fact, any transformation can
be implemented with the core API alone, by using several passes if necessary.
But this increases the transformation code complexity, this requires to store
state between passes (which can be as complex as a full tree representation!),
and parsing the class several times has a cost, which must be compared to the
cost of constructing the corresponding ClassNode.
The conclusion is that the tree API is generally used for transformations that
cannot be implemented in one pass with the core API. But there are of course
exceptions. For example an obfuscator cannot be implemented in one pass,
because you cannot transform classes before the mapping from original to
obfuscated names is fully constructed, which requires to parse all classes. But
the tree API is not a good solution either, because it would require keeping
in memory the object representation of all the classes to obfuscate. In this
case it is better to use the core API with two passes: one to compute the
mapping between original and obfuscated names (a simple hash table that
requires much less memory than a full object representation of all the classes),
and one to transform the classes based on this mapping.
So far we have only seen how to create and transform ClassNode objects, but
we haven’t seen how to construct a ClassNode from the byte array represen-
tation of a class or, vice versa, to construct this byte array from a ClassNode.
95
6. Classes
In fact this is done by composing the core API and tree API components, as
explained in this section.
6.2.1. Presentation
In addition to the fields shown in Figure 6.1, the ClassNode class extends
the ClassVisitor class, and also provides an accept method that takes a
ClassVisitor as parameter. The accept method generates events based on
the ClassNode field values, while the ClassVisitor methods perform the
inverse operation, i.e. set the ClassNode fields based on the received events:
96
6.2. Components composition
6.2.2. Patterns
Transforming a class with the tree API can be done by putting these elements
together:
ClassNode cn = new ClassNode(ASM4);
ClassReader cr = new ClassReader(...);
cr.accept(cn, 0);
... // here transform cn as you want
ClassWriter cw = new ClassWriter(0);
cn.accept(cw);
byte[] b = cw.toByteArray();
It is also possible to use a tree based class transformer like a class adapter
with the core API. Two common patterns are used for that. The first one uses
inheritance:
public class MyClassAdapter extends ClassNode {
public MyClassAdapter(ClassVisitor cv) {
super(ASM4);
this.cv = cv;
}
@Override public void visitEnd() {
// put your transformation code here
accept(cv);
}
}
the events generated by cr are consumed by the ClassNode ca, which results
in the initialization of the fields of this object. At the end, when the visitEnd
event is consumed, ca performs the transformation and, by calling its accept
method, generates new events corresponding to the transformed class, which
are consumed by cw. The corresponding sequence diagram is show in Figure
6.2, if we suppose that ca changes the class version.
When compared to the sequence diagram for ChangeVersionAdapter in Fig-
ure 2.7, we can see that the events between ca and cw occur after the events
between cr and ca, instead of simultaneously with a normal class adapter. In
97
6. Classes
fact this happens with all tree based transformations, and explains why they
are less constrained than event based ones.
The second pattern that can be used to acheive the same result, with a similar
sequence diagram, uses delegation instead of inheritance:
public class MyClassAdapter extends ClassVisitor {
ClassVisitor next;
public MyClassAdapter(ClassVisitor cv) {
super(ASM4, new ClassNode());
next = cv;
}
@Override public void visitEnd() {
ClassNode cn = (ClassNode) cv;
// put your transformation code here
cn.accept(next);
}
}
This pattern uses two objects instead of one, but works exactly in the same way
as the first pattern: the received events are used to construct a ClassNode,
which is transformed and converted back to an event based representation
98
6.2. Components composition
99
6. Classes
100
7. Methods
This chapter explains how to generate and transform methods with the ASM
tree API. It starts with a presentation of the tree API alone, with some il-
lustrative examples, and then presents how to compose it with the core API.
The tree API for generics and annotations is presented in the next chapter.
7.1.1. Presentation
The ASM tree API for generating and transforming methods is based on the
MethodNode class (see Figure 7.1).
101
7. Methods
Most of the fields of this class are similar to the corresponding fields in
ClassNode. The most important ones are the last ones, starting from the
instructions field. This field is a list of instructions, managed with an
InsnList object, whose public API is the following:
public class InsnList { // public accessors omitted
int size();
AbstractInsnNode getFirst();
AbstractInsnNode getLast();
AbstractInsnNode get(int index);
boolean contains(AbstractInsnNode insn);
int indexOf(AbstractInsnNode insn);
void accept(MethodVisitor mv);
ListIterator iterator();
ListIterator iterator(int index);
AbstractInsnNode[] toArray();
void set(AbstractInsnNode location, AbstractInsnNode insn);
void add(AbstractInsnNode insn);
void add(InsnList insns);
void insert(AbstractInsnNode insn);
void insert(InsnList insns);
void insert(AbstractInsnNode location, AbstractInsnNode insn);
void insert(AbstractInsnNode location, InsnList insns);
void insertBefore(AbstractInsnNode location, AbstractInsnNode insn);
void insertBefore(AbstractInsnNode location, InsnList insns);
void remove(AbstractInsnNode insn);
void clear();
}
102
7.1. Interfaces and components
Its sub classes are Xxx InsnNode classes corresponding to the visitXxx Insn
methods of the MethodVisitor interface, and are all build in the same way.
For instance the VarInsnNode class corresponds to the visitVarInsn method
and has the following structure:
public class VarInsnNode extends AbstractInsnNode {
public int var;
public VarInsnNode(int opcode, int var) {
super(opcode);
this.var = var;
}
...
}
Labels and frames, as well as line numbers, although they are not instructions,
are also represented by sub classes of the AbstractInsnNode classes, namely
the LabelNode, FrameNode and LineNumberNode classes. This allows them to
be inserted just before the corresponding real instructions in the list, as in the
core API (where labels and frames are visited just before their corresponding
instruction). It is therefore easy to find the target of a jump instruction, with
the getNext method provided by the AbstractInsnNode class: this is the first
AbstractInsnNode after the target label that is a real instruction. Another
consequence is that, like with the core API, removing an instruction does not
break jump instructions, as long as labels remain unchanged.
103
7. Methods
Like with classes, using the tree API to generate methods takes more time
and consumes more memory than using the core API. But it makes it possible
to generate their content in any order. In particular the instructions can be
generated in a different order than the sequential one, which can be useful in
some cases.
Consider for example an expression compiler. Normaly an expression e1 +e2 is
compiled by emitting code for e1 , then emitting code for e2 , and then emitting
code for adding the two values. But if e1 and e2 are not of the same primitive
type, a cast must be inserted just after the code for e1 , and another one just
after the code for e2 . However the exact casts that must be emitted depend
on e1 and e2 types.
Now, if the type of an expression is returned by the method that emits the
compiled code, we have a problem if we are using the core API: the cast that
must be inserted after e1 is known only after e2 has been compiled, but this
is too late because we cannot insert an instruction between previously visited
instructions1 . With the tree API this problem does not exist. For example,
one possibility is to use a compile method such as:
public Type compile(InsnList output) {
InsnList il1 = new InsnList();
InsnList il2 = new InsnList();
Type t1 = e1.compile(il1);
1
the solution is to compile expressions in two passes: one to compute the expression types
and the casts that must be inserted, and one to emit the compiled code.
104
7.1. Interfaces and components
Type t2 = e2.compile(il2);
Type t = ...; // compute common super type of t1 and t2
output.addAll(il1); // done in constant time
output.add(...); // cast instruction from t1 to t
output.addAll(il2); // done in constant time
output.add(...); // cast instruction from t2 to t
output.add(new InsnNode(t.getOpcode(IADD)));
return t;
}
Transforming a method with the tree API simply consists in modifying the
fields of a MethodNode object, and in particular the instructions list. Al-
though this list can be modified in arbitray ways, a common pattern is to mod-
ify it while iterating over it. Indeed, unlike with the general ListIterator
contract, the ListIterator returned by an InsnList supports many conccur-
rent2 list modifications. In fact you can use the InsnList methods to remove
one or more elements before and including the current one, to remove one or
more elements after the next element (i.e. not just after the current element,
but after its successor), or to insert one or more elements before the current
one or after its successor. These changes will be reflected in the iterator, i.e.
the elements inserted (resp. removed) after the next element will be seen (resp.
not seen) in the iterator.
Another common pattern to modify an instruction list, when you need to
insert several instructions after an instruction i inside a list, is to add these
new instructions in a temporary instruction list, and to insert this temporary
list inside the main one in one step:
Inserting the instructions one by one is also possible but more cumbersome,
because the insertion point must be updated after each insertion.
2
i.e. modifications interleaved with calls to Iterator.next. True, multi-threaded concur-
rent modifications are not supported.
105
7. Methods
106
7.1. Interfaces and components
You can see here the pattern discussed in the previous section for inserting
several instructions in the instruction list, which consists in using a temporary
instruction list. This example also shows that it is possible to insert instruc-
tions before the current one while iterating over an instruction list. Note that
the amount of code that is necessary to implement this adapter is approxima-
tively the same with the core and tree APIs.
The method adapter that removes field self assignments (see section 3.2.5) can
be implemented as follows (if we suppose that MethodTransformer is similar
to the ClassTransformer class of the previous chapter):
public class RemoveGetFieldPutFieldTransformer extends
MethodTransformer {
public RemoveGetFieldPutFieldTransformer(MethodTransformer mt) {
super(mt);
}
@Override public void transform(MethodNode mn) {
InsnList insns = mn.instructions;
Iterator<AbstractInsnNode> i = insns.iterator();
while (i.hasNext()) {
AbstractInsnNode i1 = i.next();
if (isALOAD0(i1)) {
AbstractInsnNode i2 = getNext(i1);
if (i2 != null && isALOAD0(i2)) {
AbstractInsnNode i3 = getNext(i2);
if (i3 != null && i3.getOpcode() == GETFIELD) {
AbstractInsnNode i4 = getNext(i3);
if (i4 != null && i4.getOpcode() == PUTFIELD) {
if (sameField(i3, i4)) {
while (i.next() != i4) {
}
insns.remove(i1);
insns.remove(i2);
insns.remove(i3);
insns.remove(i4);
}
}
}
}
}
}
super.transform(mn);
}
private static AbstractInsnNode getNext(AbstractInsnNode insn) {
do {
107
7. Methods
insn = insn.getNext();
if (insn != null && !(insn instanceof LineNumberNode)) {
break;
}
} while (insn != null);
return insn;
}
private static boolean isALOAD0(AbstractInsnNode i) {
return i.getOpcode() == ALOAD && ((VarInsnNode) i).var == 0;
}
private static boolean sameField(AbstractInsnNode i,
AbstractInsnNode j) {
return ((FieldInsnNode) i).name.equals(((FieldInsnNode) j).name);
}
}
108
7.1. Interfaces and components
AbstractInsnNode i3 = getNext(i);
while (i3 != null && isALOAD0(i3)) {
i1 = i2;
i2 = i3;
i3 = getNext(i);
}
if (i3 != null && i3.getOpcode() == GETFIELD) {
AbstractInsnNode i4 = getNext(i);
if (i4 != null && i4.getOpcode() == PUTFIELD) {
if (sameField(i3, i4)) {
insns.remove(i1);
insns.remove(i2);
insns.remove(i3);
insns.remove(i4);
}
}
}
}
}
}
super.transform(mn);
}
private static AbstractInsnNode getNext(Iterator i) {
while (i.hasNext()) {
AbstractInsnNode in = (AbstractInsnNode) i.next();
if (!(in instanceof LineNumberNode)) {
return in;
}
}
return null;
}
...
}
The difference with the previous implementation is the getNext method, which
now acts on the list iterator. When the sequence is recognized the iterator is
just after it, so the while (i.next() != i4) loop is no longer necessary. But
here the special case of three or more successive ALOAD 0 instructions shows
up again (see the while (i3 != null) loop).
All the method transformations that we have seen so far were local, even the
statefull ones, in the sense that the transformation of an instruction i only
depended on instructions at a fixed distance from i. There are however global
109
7. Methods
110
7.1. Interfaces and components
}
}
This code works as follows: when a jump instruction in is found, its target
is stored in label. Then the instruction that comes just after this label is
searched for with the innermost while loop (AbstractInsnNode objects that
do not represent real instructions, such as FrameNode or LabelNode, have a
negative “opcode”). As long as this instruction is a GOTO, label is replaced
with the target of this instruction, and the previous steps are repeated. Finally
the target label of in is replaced with this updated label value and, if in is
itself a GOTO and if its updated target is a RETURN instruction, in is replaced
with a clone of this return instruction (recall that an instruction object cannot
appear more than once in an instruction list).
The effect of this transformation on the checkAndSetF method defined in
section 3.1.5 is shown below:
// before // after
ILOAD 1 ILOAD 1
IFLT label IFLT label
ALOAD 0 ALOAD 0
ILOAD 1 ILOAD 1
PUTFIELD ... PUTFIELD ...
GOTO end RETURN
label: label:
F_SAME F_SAME
NEW ... NEW ...
DUP DUP
INVOKESPECIAL ... INVOKESPECIAL ...
ATHROW ATHROW
end: end:
F_SAME F_SAME
RETURN RETURN
Note that, although this transformation changes the jump instructions (more
formally the control flow graph), it does not need to update the method’s
frames. Indeed the state of the execution frame remains the same at each
instruction and, since no new jump target is introduced, no new frame must
be visited. It may happen, however, that a frame is no longer needed. For
instance, in the above example, the end label is no longer used after trans-
formation, as well as the F_SAME frame and the RETURN instruction after it.
Hopefully it is perfectly legal to visit more frames than is strictly necessary,
as well as to include unused code – called dead or unreachable code – in a
method. The above method adapter is therefore correct, even if it could be
111
7. Methods
So far we have only seen how to create and transform MethodNode objects, but
we haven’t seen the link with the byte array representation of classes. Like for
classes, this link is done by composing the core API and tree API components,
as explained in this section.
7.2.1. Presentation
In addition to the fields shown in Figure 7.1 the MethodNode class extends
the MethodVisitor class, and also provides two accept methods that take a
MethodVisitor or a ClassVisitor as parameter. The accept methods gen-
erate events based on the MethodNode field values, while the MethodVisitor
methods perform the inverse operation, i.e. set the MethodNode fields based
on the received events.
7.2.2. Patterns
Like for classes, it is possible to use a tree based method transformer like a
method adapter with the core API. The two patterns that can be used for
classes are indeed also valid for methods, and work exactly in the same way.
The pattern based on inheritance is the following:
public class MyMethodAdapter extends MethodNode {
public MyMethodAdapter(int access, String name, String desc,
String signature, String[] exceptions, MethodVisitor mv) {
super(ASM4, access, name, desc, signature, exceptions);
this.mv = mv;
}
@Override public void visitEnd() {
// put your transformation code here
accept(mv);
}
}
112
7.2. Components composition
A variant of the first pattern is to use it with an anonymous inner class directly
in the visitMethod of a ClassAdapter:
public MethodVisitor visitMethod(int access, String name,
String desc, String signature, String[] exceptions) {
return new MethodNode(ASM4, access, name, desc, signature, exceptions)
{
@Override public void visitEnd() {
// put your transformation code here
accept(cv);
}
};
}
These patterns show that it is possible to use the tree API only for methods,
and the core API for classes. In practice this strategy is often used.
113
7. Methods
114
8. Method Analysis
This chapter presents the ASM API for analyzing the code of methods, which is
based on the tree API. It starts with a presentation of code analysis algorithms,
and then presents the corresponding ASM API, with some examples.
8.1. Presentation
Code analysis is a very large topic, and many algorithms exist for analyzing
code. It would be impossible and out of the scope of this document to present
them all here. In fact the goal of this section is just to give an overview of
the algorithms that are used in ASM. A better presentation of this topic can
be found in books about compilers. The next sections present two important
types of code analysis techniques, namely data flow and control flow analysis:
• A data flow analysis consists in computing the state of the execution
frames of a method, for each instruction of this method. This state can
be represented in a more or less abstract way. For example reference
values can be represented by a single value, by one value per class, by
three possible values in the { null, not null, may be null } set, etc.
• A control flow analysis consists in computing the control flow graph of
a method, and in performing analyses on this graph. The control flow
graph is a graph whose nodes are instructions, and whose oriented edges
connect two instructions i → j if j can be executed just after i.
115
8. Method Analysis
• a backward analysis computes, for each instruction, the state of the ex-
ecution frame before this instruction, from the state after its execution.
• combining them,
This looks like what an interpreter or the Java Virtual Machine does, but
in fact it is completely different because the goal is to simulate all potential
execution paths in a method, for all possible argument values, instead of the
single execution path determined by some specific method argument values.
One consequence is that, for branch instructions, both branches are simulated
(while a real interpreter follows only one branch, depending on the actual
condition value).
Another consequence is that the manipulated values are in fact sets of possible
values. These sets can be very large, such as “all possible values”, “all the
integers”, “all possible objects” or “all possible String objects”, in which case
they can also be called types. They can also be more precise, such as “all
positive integers”, “all integers between 0 and 10” or “all possible non null
objects”. Simulating the execution of an instruction i consists in finding the
set of all possible results of i, for all combinations of values in its operand
value sets. For instance, if integers are represented by three sets P = “positive
or null”, N = “negative or null”, and A = “all integers”, simulating the IADD
instruction means returning P if both operands are P, N if both operands are
N, and A in all other cases.
A last consequence is the need to compute unions of sets of values: for example
the set of possible values corresponding to (b ? e1 : e2) is the union of
the possible values of e1 and of the possible values of e2. More generally this
operation is needed each time the control flow graph contains two or more
edges with a common destination. In the previous example, where integers
are represented by the three sets P, N, and A, computing the union of two of
these sets is easy: it is always A, unless the two sets are equal.
116
8.2. Interfaces and components
This graph can be decomposed in four basic blocks (shown above with rectan-
gles), a basic block being a sequence of instructions such that each instruction
except the last one has exactly one successor, and such that no instruction
except the first one can be the target of a jump.
117
8. Method Analysis
118
8.2. Interfaces and components
this.owner = owner;
next = mv;
}
@Override public void visitEnd() {
MethodNode mn = (MethodNode) mv;
Analyzer<BasicValue> a =
new Analyzer<BasicValue>(new BasicInterpreter());
try {
a.analyze(owner, mn);
Frame<BasicValue>[] frames = a.getFrames();
AbstractInsnNode[] insns = mn.instructions.toArray();
for (int i = 0; i < frames.length; ++i) {
if (frames[i] == null && !(insns[i] instanceof LabelNode)) {
mn.instructions.remove(insns[i]);
}
}
} catch (AnalyzerException ignored) {
}
mn.accept(next);
}
}
Note that dead labels are not removed. This is done on purpose: indeed it
does not change the resulting code, but avoids removing a label that, although
not reachable, might be referenced in a LocalVariableNode, for instance.
119
8. Method Analysis
120
8.2. Interfaces and components
referenced by a method into the JVM. This default behavior can be changed
by overriding the protected methods of this class.
Like BasicVerifier, this class could be used during the development of a
class generator or adapter in order find bugs more easily. But it can also
be used for other purposes. One example is a transformation that removes
unnecessary casts in methods: if this analyzer finds that the operand of a
CHECKCAST to instruction is the set of values “all objects of type from”, and if
to is a super class of from, then the CHECKCAST instruction is unnecessary and
can be removed. The implementation of this transformation is the following:
public class RemoveUnusedCastTransformer extends MethodTransformer {
String owner;
public RemoveUnusedCastTransformer(String owner,
MethodTransformer mt) {
super(mt);
this.owner = owner;
}
@Override public MethodNode transform(MethodNode mn) {
Analyzer<BasicValue> a =
new Analyzer<BasicValue>(new SimpleVerifier());
try {
a.analyze(owner, mn);
Frame<BasicValue>[] frames = a.getFrames();
AbstractInsnNode[] insns = mn.instructions.toArray();
for (int i = 0; i < insns.length; ++i) {
AbstractInsnNode insn = insns[i];
if (insn.getOpcode() == CHECKCAST) {
Frame f = frames[i];
if (f != null && f.getStackSize() > 0) {
Object operand = f.getStack(f.getStackSize() - 1);
Class<?> to = getClass(((TypeInsnNode) insn).desc);
Class<?> from = getClass(((BasicValue) operand).getType());
if (to.isAssignableFrom(from)) {
mn.instructions.remove(insn);
}
}
}
}
} catch (AnalyzerException ignored) {
}
return mt == null ? mn : mt.transform(mn);
}
private static Class<?> getClass(String desc) {
try {
return Class.forName(desc.replace(’/’, ’.’));
} catch (ClassNotFoundException e) {
throw new RuntimeException(e.toString());
121
8. Method Analysis
}
}
private static Class<?> getClass(Type t) {
if (t.getSort() == Type.OBJECT) {
return getClass(t.getInternalName());
}
return getClass(t.getDescriptor());
}
}
122
8.2. Interfaces and components
the first line prevents some compilers from detecting the bug, which would
otherwise be detected as an “o may not have been initialized” error):
Object o = null;
while (...) {
o = ...;
}
o.m(...); // potential NullPointerException!
Then we need a data flow analysis that can tell us that, at the INVOKEVIRTUAL
instruction corresponding to the last line, the bottom stack value, correspond-
ing to o, may be null. In order to do that we need to distinguish three sets
for reference values: the NULL set containing the null value, the NONNULL set
containing all non null reference values, and the MAYBENULL set containing all
the reference values. Then we just need to consider that ACONST_NULL pushes
the NULL set on the operand stack, while all other instructions that push a
reference value on the stack push the NONNULL set (in other words we consider
that the result of any field access or method call is not null – we cannot
do better without a global analysis of all the classes of the program). The
MAYBENULL set is necessary to represent the union of the NULL and NONNULL
sets.
The above rules must be implemented in a custom Interpreter subclass.
It would be possible to implement it from scratch, but it is also possible,
and much easier, to implement it by extending the BasicInterpreter class.
Indeed, if we consider that BasicValue.REFERENCE_VALUE corresponds to the
NONNULL set, then we just need to override the method that simulates the
execution of ACONST_NULL, so that it returns NULL, as well as the method that
computes set unions:
class IsNullInterpreter extends BasicInterpreter {
public final static BasicValue NULL = new BasicValue(null);
public final static BasicValue MAYBENULL = new BasicValue(null);
public IsNullInterpreter() {
super(ASM4);
}
@Override public BasicValue newOperation(AbstractInsnNode insn) {
if (insn.getOpcode() == ACONST_NULL) {
return NULL;
}
return super.newOperation(insn);
}
@Override public BasicValue merge(BasicValue v, BasicValue w) {
if (isRef(v) && isRef(w) && v != w) {
return MAYBENULL;
}
123
8. Method Analysis
124
8.2. Interfaces and components
125
8. Method Analysis
Then we can provide an Analyzer subclass that constructs our control flow
graph, and use its result to compute the number of edges, the number of nodes,
and finally the cyclomatic complexity:
public class CyclomaticComplexity {
public int getCyclomaticComplexity(String owner, MethodNode mn)
throws AnalyzerException {
Analyzer<BasicValue> a =
new Analyzer<BasicValue>(new BasicInterpreter()) {
protected Frame<BasicValue> newFrame(int nLocals, int nStack) {
return new Node<BasicValue>(nLocals, nStack);
}
protected Frame<BasicValue> newFrame(
Frame<? extends BasicValue> src) {
return new Node<BasicValue>(src);
}
protected void newControlFlowEdge(int src, int dst) {
Node<BasicValue> s = (Node<BasicValue>) getFrames()[src];
s.successors.add((Node<BasicValue>) getFrames()[dst]);
}
};
a.analyze(owner, mn);
Frame<BasicValue>[] frames = a.getFrames();
int edges = 0;
int nodes = 0;
for (int i = 0; i < frames.length; ++i) {
if (frames[i] != null) {
edges += ((Node<BasicValue>) frames[i]).successors.size();
nodes += 1;
}
}
return edges - nodes + 2;
}
}
126
9. Metadata
This chapter presents the tree API for compiled Java classes metadata, such
as annotations. It is very short because these metadata have already been pre-
sented in chapter 4, and because the tree API is simple once the corresponding
core API is known.
9.1. Generics
The tree API does not provide any support for generic types! Indeed it repre-
sents generic types with signatures, as in the core API, but does not provide
a SignatureNode class corresponding to SignatureVisitor, although this
would be possible (in fact it would be convenient to use several Node classes
to distinguish between type, method and class signatures, at least).
9.2. Annotations
The tree API for annotations is based on the AnnotationNode class, whose
public API is the following:
public class AnnotationNode extends AnnotationVisitor {
public String desc;
public List<Object> values;
public AnnotationNode(String desc);
public AnnotationNode(int api, String desc);
... // methods of the AnnotationVisitor interface
public void accept(AnnotationVisitor av);
}
The desc field contains the annotation type, while the values field contains
the name value pairs, where each name is followed by its associated value (the
representation of values is described in the Javadoc).
As you can see the AnnotationNode class extends the AnnotationVisitor
class, and also provides an accept method that takes as parameter an object
127
9. Metadata
of this type, like the ClassNode and MethodNode classes with the class and
method visitor classes. The patterns that we have seen for classes and methods
can therefore also be used for composing the core and tree API components for
annotations. For example the “anonymous inner class” variant of the pattern
based on inheritance (see section 7.2.2), adapted to annotations, gives:
public AnnotationVisitor visitAnnotation(String desc, boolean visible) {
return new AnnotationNode(ASM4, desc) {
@Override public void visitEnd() {
// put your annotation transformation code here
accept(cv.visitAnnotation(desc, visible));
}
};
}
9.3. Debug
The source file from which a class was compiled is stored in the sourceFile
field in ClassNode. The information about source line numbers is stored in
LineNumberNode objects, whose class inherits from AbstractInsnNode. Sim-
ilarly to the core API, where information about line numbers is visited at the
same time as instructions, LineNumberNode objects are part of the instruc-
tion list. Finally the name and type of source local variables is stored in the
MethodNode’s localVariables field, which is a list of LocalVariableNode
objects.
128
10. Backward compatibility
10.1. Introduction
As with the core API, a new mechanism has been introduced in the tree API
in ASM 4.0, in order to ensure backward compatibility in the future ASM
versions. However, here again, this property can not be ensured by ASM
alone. It requires users to follow a few simple guidelines when writing their
code. The goal of this chapter is to present these guidelines, and to give an
idea of the internal mechanism used in the ASM tree API to ensure backward
compatibility.
10.2. Guidelines
This section presents the guidelines that you must follow when using the ASM
tree API, in order to ensure that your code will remain valid with any future
ASM versions (in the sense of the contract defined in section 5.1.1).
First of all, if you write a class generator using the tree API, there is no
guideline to follow (as with the core API). You can create the ClassNode and
other elements with any constructor, and use any method of these classes.
If, on the other hand, you write a class analyzer or a class adapter with the tree
API, i.e., if you use a ClassNode or other similar classes populated directly or
indirectly via a ClassReader.accept(), or if you override one of these classes,
then you must follow a few guidelines, presented below.
We consider here the case where you create a ClassNode, populate it via a
ClassReader, and then analyze or transform it before optionally writing the
129
10. Backward compatibility
result with a ClassWriter (the discussion and guidelines are the same for the
other node classes; analyzing or transforming a ClassNode created by someone
else is discussed in the next section). In this case there is only one guideline:
Guideline 3: to write a class analyzer or adapter with the tree API of ASM
version X, create your ClassNode by using the constructor with this ex-
act version as argument (as opposed to the default constructor, without
parameters).
The goal of this guideline is to throw an error as soon as an unknown feature is
encountered when populating the ClassNode via a ClassReader (as defined in
the backward compatibility contract). If you do not follow it, your analysis or
transformation code may fail later when encountering an unknown element, or
it may succeed but produce a wrong result because it should not have ignored
these unknown elements. In other words, the last clause of the contract may
not be ensured if this guideline is not followed.
How does this work? Internally, ClassNode is implemented as follows in ASM
4.0 (we reuse here the example of section 5.1.2):
public class ClassNode extends ClassVisitor {
public ClassNode() {
super(ASM4, null);
}
public ClassNode(int api) {
super(api, null);
}
...
public void visitSource(String source, String debug) {
// store source and debug in local fields ...
}
}
130
10.2. Guidelines
visitSource(source, debug);
else
throw new RuntimeException();
} else {
// store author, source and debug in local fields ...
}
}
public void visitLicense(String license) {
if (api < ASM5) throw new RuntimeException();
// store license in local fields ...
}
}
If you use ASM 4.0, creating a ClassNode(ASM4) does nothing special. But
if you upgrade to ASM 5.0, without changing your code, you will get a
ClassNode 5.0 whose api field will be ASM4 < ASM5. It is then easy to see that
if the input class contains a non null author or license attribute, populating
the ClassNode via a ClassReader will fail, as defined in our contract. If you
also upgrade your code, changing the api field to ASM5 and also updating the
rest of the code to take these new attributes into account, then no errors will
be thrown when populating the node.
Note that the ClassNode 5.0 code is very similar to the ClassVisitor 5.0 code.
This is to ensure a proper semantics if you define subclasses of ClassNode
(similarly to subclasses of ClassVisitor - see section 10.2.2).
131
10. Backward compatibility
method with this exact version as argument before using the ClassNode
in any way.
The goal is the same as for guideline 3: the last clause of the contract may not
be ensured if this guideline is not followed. How does this work? Internally,
the check method is implemented as follows in ASM 4.0
public class ClassNode extends ClassVisitor {
...
public void check(int api) {
// nothing to do
}
}
If your code is written for ASM 4.0, and if you get a ClassNode 4.0, whose api
field will be ASM4, there will be no problem and check does nothing. But if you
get a ClassNode 5.0, the check(ASM4) method will fail if this node actually
contains a non null author or license, i.e. if it contains new features that
were unknown in ASM 4.0.
Note: this guideline can also be used if you create the ClassNode yourself.
Then you don’t need to follow guideline 3, i.e., you don’t need to specify
an ASM version in the ClassNode constructor. The checks will occur
instead in the check method (but this may be less efficient that doing
the checks earlier, when populating the ClassNode).
132
10.2. Guidelines
The classes in asm.util and asm.commons have two variants of each construc-
tor: one with and one without an ASM version parameter.
If you simply want to instantiate and use as is the ASMifier, Textifier, or
CheckXxx Adapter classes in asm.util, or any class in the asm.commons pack-
age, then you can instantiate them with a constructor without an ASM version
parameter. You could also use a constructor with an ASM version parameter,
but this would unnecessarily restrict these components to the specified ASM
version (while using the no-arg constructor is equivalent to say “use the latest
ASM version”). This is why the constructors using an ASM version parameter
are declared protected.
If, on the other hand, you want to override the ASMifier, TextifierVisitor,
or CheckXxxAdapter classes in asm.util, or any class in the asm.commons
package, then the guidelines 1 and 2 apply. In particular, your constructor
must call super(...) with the ASM version you want to use as parameter.
Finally, the same distinction must be made if you want to use vs. override
the Interpreter class or its subclasses in asm.tree.analysis. Note also
that before using the analysis package you must create a MethodNode or get
one from someone else, and that guidelines 3 and 4 must be used here before
passing this node to an Analyzer.
133
10. Backward compatibility
134
A. Appendix
Local variables
Stack
135
A. Appendix
Constants
136
A.1. Bytecode instructions
Casts
Arrays
NEWARRAY type (for any primitive type) ... , n ... , new type[n]
ANEWARRAY class ... , n ... , new class[n]
MULTIANEWARRAY [...[t n ... , i1 , ... , in ... , new t[i1 ]...[in ]...
137
A. Appendix
Jumps
Return
138
A.2. Subroutines
A.2. Subroutines
The first instruction pushes as return address the index of the second instruc-
tion, and jumps to the ASTORE instruction. This instruction stores the return
address in local variable 1. Then local variable 0 is incremented by one. Fi-
nally the RET instruction loads the return address contained in local variable
1 and jumps to the corresponding instruction, i.e., the second instruction.
This second instruction is again a JSR instruction: it pushes as return address
the index of the third instruction, and jumps to the ASTORE instruction. When
the RET instruction is reached again the return address corresponds now to the
RETURN instruction, and so execution jumps to this RETURN and stops.
The instructions after the sub label define what is called a subroutine. It is
like a little “method”, which can be “called” from different places, inside a
139
A. Appendix
140
A.3. Attributes
A.3. Attributes
141
A. Appendix
The most important methods are the read and write methods. The read
method decodes the raw content of attributes of this type, and the write
method performs the inverse operation. Note that the read method must
return a new attribute instance. In order to decode attributes of this type
when reading a class, you must use:
ClassReader cr = ...;
ClassVisitor cv = ...;
cr.accept(cv, new Attribute[] { new CommentAttribute("") }, 0);
142
A.4. Guidelines
A.4. Guidelines
We recall here the guidelines that must be followed in order to ensure that
your code will be backward compatible with older ASM versions (see chapters
5 and 10).
Guideline 1: to write a ClassVisitor subclass for ASM version X, call the
ClassVisitor constructor with this exact version as argument, and
never override or call methods that are deprecated in this version of the
ClassVisitor class (or that are introduced in later versions).
Guideline 2: do not use inheritance of visitors, use delegation instead (i.e.
visitor chains). A good practice is to make your visitor classes final by
default to ensure this.
Guideline 3: to write a class analyzer or adapter with the tree API of ASM
version X, create your ClassNode by using the constructor with this ex-
act version as argument (as opposed to the default constructor, without
parameters).
Guideline 4: to write a class analyzer or adapter with the tree API of ASM
version X, using a ClassNode created by someone else, call its check()
method with this exact version as argument before using the ClassNode
in any way.
Guidelines 1 and 2 also apply for subclasses of ClassNode, MethodNode, etc,
of Interpreter and its subclasses in asm.tree.analysis, of the ASMifier,
Texifier, or CheckXxx Adapter classes in asm.util, and of any class in the
asm.commons package. Finally, there are two exceptions to guideline 2:
• you can use inheritance of visitors if you fully control the inheritance
chain yourself, and release all the classes of the hierarchy at the same
time. You must then ensure that all the classes in the hierarchy are
written for the same ASM version. Still, make the leaf classes of your
hierarchy final.
• you can use inheritance of “visitors” if no class except the leaf ones
override any visit method (for instance, if you use intermediate classes
between ClassVisitor and the concrete visitor classes only to introduce
convenience methods). Still, make the leaf classes of your hierarchy final
(unless they do not override any visit method either; in this case provide
a constructor taking an ASM version as argument so that subclasses can
specify for which version they are written).
143
A. Appendix
144
A.5. Performances
A.5. Performances
The figure below gives the relative performances of the core and tree APIs, of
the ClassWriter options, and of the analysis framework (shorter is faster):
145
A. Appendix
disk and to load them inside the JVM is not taken into account. The results
were obtained by running each test ten times, on the 18600+ classes of JDK
7 rt.jar, and by using the performance of the best run.
A quick analysis of these results shows that:
• 90% of the transformation time is due to class parsing and writing.
• The “copy constant pool” optimization gives a 15-20% speed up.
• Tree based transformations are about 25% slower than visitor based ones.
• The COMPUTE_MAXS option does not cost too much.
• The COMPUTE_FRAMES option costs a lot ⇒ do incremental frame updates.
• The cost of the analysis package is quite high!
146
Index
147
Index
fields Opcodes, 16
adding, 23, 94 operand, 33
constant value, 17 operand stack, 31
removing, 22 size, 32
FieldVisitor, 13 updating size, 50
Frame, 117
frames, 39 PatternMethodAdapter, 54
compression, 40
signature, 67
computing, 44
SignatureReader, 71
skipping, 79
SignatureVisitor, 70
uncompressing, 79
SignatureWriter, 71
updating, 51, 58, 64, 111
SimpleVerifier, 120
generic classes, 67 static blocks, 59
opcode, 33
148