Search code examples
javajava-bytecode-asmbytecode-manipulationjvm-bytecode

How does one create an ASM LdcInsnNode that statically adds the current class to the stack?


I'm using the ASM library to modify bytecode created by others. For an arbitrary method in an arbitrary class, I'd like to create an LdcInsnNode that adds the current class to the stack.

For example, let's say I'm transforming a class called com.example.ExampleClass. I'd like to create bytecode that is equivalent to System.out.println(ExampleClass.class.getName());.

This seems like a relatively simple task. When I use the Eclipse Bytecode Outline plugin, it says that the following bytecode is equivalent:

GETSTATIC java/lang/System.out : Ljava/io/PrintStream;
LDC Lcom/example/ExampleClass;.class
INVOKEVIRTUAL java/lang/Class.getName ()Ljava/lang/String;
INVOKEVIRTUAL java/io/PrintStream.println (Ljava/lang/Object;)V

I tried the following code:

private InsnList printClass() {
    InsnList result = new InsnList();
    result.add(new FieldInsnNode(Opcodes.GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;"));
    result.add(new LdcInsnNode("L" + name + ";.class"));
    result.add(new MethodInsnNode(Opcodes.INVOKEVIRTUAL, "java/lang/Class", "getName", "()Ljava/lang/String;", false));
    result.add(new MethodInsnNode(Opcodes.INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V", false));
    return result;
}

This is being run in an extension of ClassNode, so name refers to the ClassNode.name field. The InsnList returned by this method is being inserted before an existing AbstractInsnNode using InsnList.insertBefore(AbstractInsnNode, printClass()). When this point is reached in the bytecode, I get an error with the following reason:

Type 'java/lang/String' (current frame, stack[1]) is not assignable to 'java/lang/Class'

This is clearly because the LDC instruction is adding the String "Lcom/example/ExampleClass;.class" instead of the actual class Lcom/example/ExampleClass;.class.

Is there any workaround for this? It seems impossible to directly add the Class object to an LdcInsnNode because the class doesn't exist yet. But is there a way to add an instruction that loads the Class object?

In my particular case, calling the Object.getClass() method is not an option because it needs to work from a static context.


Solution

  • You don’t need to refer to a Class object, in fact, it isn’t even supported (directly). If you want to push a Class to the operand stack via ASM, you have to refer to it as Type instance.

    E.g.

    new LdcInsnNode(Type.getObjectType(name))
    

    using the Type.getObjectType(…) factory method, which expects name to represent the internal name form, e.g. com/example/ExampleClass without L … ; around it. It’s equivalent to Type.getType('L'+name+';') for non-array types. The choice of the factory method is important as a Type object may represent primitive types or method types as well. Since both, ldc and Type.getObjectType, only support reference types, this is the natural combination.

    It seems the LdcInsnNode constructor documentation is a bit outdated, as it only mentions number types and String (Class support with ldc exists since Java 5, but the documentation contains links to 1.4.2). You may refer to MethodVisitor.visitLdcInsn(…) instead, to which the object is eventually passed when producing the byte code.