I'm trying to generate a simple conditional Jump instruction. Here is the class:
public static Class<?> getKlass2(){
String className = "TestClass";
ClassWriter classWriter = new ClassWriter(ClassWriter.COMPUTE_MAXS);
classWriter.visit(V1_8, ACC_PUBLIC, className, null, getInternalName(Object.class), null);
MethodVisitor mv = classWriter.visitMethod(ACC_PUBLIC + ACC_STATIC, "m", "()Z",null, null);
Label trueLable = new Label();
Label afterFalseLable = new Label();
mv.visitFieldInsn(GETSTATIC, getInternalName(Boolean.class), "TRUE", "Ljava/lang/Boolean;");
mv.visitMethodInsn(INVOKEVIRTUAL, getInternalName(Boolean.class), "booleanValue", "()Z", false);
mv.visitJumpInsn(IFEQ, trueLable);
mv.visitInsn(ICONST_1);
mv.visitJumpInsn(GOTO, afterFalseLable);
mv.visitLabel(trueLable);
mv.visitInsn(ICONST_0);
mv.visitFrame(F_APPEND, 0, null, 0, null);
mv.visitLabel(afterFalseLable);
mv.visitInsn(IRETURN);
mv.visitMaxs(1, 1);
mv.visitEnd();
//converting classWriter.toByteArray() to Class<?> instance
}
When loading the class I got the following error:
Expecting a stackmap frame at branch target 13
Exception Details:
Location:
TestClass.m()Z @6: ifeq
Reason:
Expected stackmap frame at this location.
Bytecode:
0x0000000: b200 0cb6 000f 9900 0704 a700 0403 ac
Stackmap Table:
same_frame_extended(@14)
But the code of the class seems ok to me:
public class TestClass {
public static boolean m();
Code:
0: getstatic #12 // Field java/lang/Boolean.TRUE:Ljava/lang/Boolean;
3: invokevirtual #15 // Method java/lang/Boolean.booleanValue:()Z
6: ifeq 13
9: iconst_1
10: goto 14
13: iconst_0
14: ireturn
}
So I tried to add a frame by hand:
mv.visitFrame(F_APPEND, 0, new Object[]{ }, 0, new Object[]{ trueLabel });
But it also failed with the same exception. Do they expect to recreate a frame with the same operand stack? But this seems non-sense since I cannot get a direct access to opearand stack from Java code.
What did I do wrong?
You may simply specify COMPUTE_FRAMES
to the ClassWriter
’s constructor to let ASM calculate both, max stack&locals and the stack map table frame entries for you. As the documentation states, “…computeFrames implies computeMaxs”.
However, I always recommend trying to understand the stack map, as the calculation from scratch is not only expensive, there are fundamental limitations (as elaborated in this answer). Since you should already have an idea of what the stack frame ought to look like, it shouldn’t be too hard to encode that knowledge. Since this also implies knowing the maximum number of local variables and operand stack entries, it would be consistent to specify them manually too.
Your attempted solution, however, is way off:
mv.visitFrame(F_APPEND, 0, new Object[]{ }, 0, new Object[]{ trueLabel });
F_APPEND
implies that new variables have been added, which doesn’t match your obvious intention of adding a stack entry. Further, specifying a label as stack entry is only valid if you are referring to the location of a NEW
instruction, to denote an uninitialized object. But here, you have pushed an INTEGER
value.
The correct code would look like:
String className = "TestClass";
ClassWriter classWriter = new ClassWriter(0);
classWriter.visit(V1_8, ACC_PUBLIC, className, null, getInternalName(Object.class), null);
MethodVisitor mv = classWriter.visitMethod(ACC_PUBLIC | ACC_STATIC, "m", "()Z",null, null);
Label trueLabel = new Label();
Label afterFalseLabel = new Label();
mv.visitFieldInsn(GETSTATIC, getInternalName(Boolean.class),"TRUE","Ljava/lang/Boolean;");
mv.visitMethodInsn(INVOKEVIRTUAL,getInternalName(Boolean.class),"booleanValue","()Z",false);
mv.visitJumpInsn(IFEQ, trueLabel);
mv.visitInsn(ICONST_1);
mv.visitJumpInsn(GOTO, afterFalseLabel);
// target of IFEQ, the frame matches the initial frame (no variables, no stack entries)
mv.visitFrame(F_SAME, 0, null, 0, null);
mv.visitLabel(trueLabel);
mv.visitInsn(ICONST_0);
// merge point of the two branches, now having an INTEGER on the stack
mv.visitFrame(F_SAME1, 0, null, 1, new Object[]{ INTEGER });
mv.visitLabel(afterFalseLabel);
mv.visitInsn(IRETURN);
// no variable at all, at most one stack entry (the integer)
mv.visitMaxs(1, 0);
mv.visitEnd();
//converting classWriter.toByteArray() to Class<?> instance
Note that for the special compressed frame types, most of the arguments are implied, for F_SAME
, all other arguments are irrelevant, for F_SAME1
, only the specified new stack entry type in the last argument matters.
But you don’t need to deal with the different types of compressed frames. If in doubt, you could always specify F_NEW
with a full description of the assumed stack frame layout. The only difference is a (slightly) bigger class file. For dynamic class generation, this may be entirely irrelevant and even for generated classes added to an application before deployment, the difference may be negligible:
String className = "TestClass";
ClassWriter classWriter = new ClassWriter(0);
classWriter.visit(V1_8, ACC_PUBLIC, className, null, getInternalName(Object.class), null);
MethodVisitor mv = classWriter.visitMethod(ACC_PUBLIC | ACC_STATIC, "m", "()Z",null, null);
Label trueLabel = new Label();
Label afterFalseLabel = new Label();
mv.visitFieldInsn(GETSTATIC, getInternalName(Boolean.class),"TRUE","Ljava/lang/Boolean;");
mv.visitMethodInsn(INVOKEVIRTUAL,getInternalName(Boolean.class),"booleanValue","()Z",false);
mv.visitJumpInsn(IFEQ, trueLabel);
mv.visitInsn(ICONST_1);
mv.visitJumpInsn(GOTO, afterFalseLabel);
// target of IFEQ, the frame state is "no variables, no stack entries"
mv.visitFrame(F_NEW, 0, null, 0, null);
mv.visitLabel(trueLabel);
mv.visitInsn(ICONST_0);
// merge point of the two branches, frame state is "no variables, one INTEGER on the stack"
mv.visitFrame(F_NEW, 0, null, 1, new Object[]{ INTEGER });
mv.visitLabel(afterFalseLabel);
mv.visitInsn(IRETURN);
// no variable at all, at most one stack entry (the integer)
mv.visitMaxs(1, 0);
mv.visitEnd();
By the way, I find it a bit odd to combine abstracted name generation, like getInternalName( Boolean.class)
, with hardcoded signatures like "Ljava/lang/Boolean;"
. Both is valid, but it would be better to consistently decide for either way.