Search code examples
javabytecodejava-bytecode-asm

Java ASM GeneratorAdapter variable naming


I am generating a simple class and unable to inject a proper variable name. ASM version is 5.2.

Here is the code:

package com.test;

import org.objectweb.asm.*;
import org.objectweb.asm.commons.GeneratorAdapter;
import org.objectweb.asm.commons.Method;

import java.nio.file.Files;
import java.nio.file.Paths;

public class Main {

    public static void main(String[] args) throws Exception {
        ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_FRAMES);
        String name = "com.test.Sub";
        cw.visit(Opcodes.V1_8, Opcodes.ACC_PUBLIC, name.replace('.', '/'), null, "java/lang/Object", null);
        Method ctor = Method.getMethod("void <init>()");
        GeneratorAdapter mg = new GeneratorAdapter(Opcodes.ACC_PUBLIC, ctor, null, null, cw);
        mg.visitCode();
        mg.loadThis();
        mg.invokeConstructor(Type.getType(Object.class), ctor);
        int var = mg.newLocal(Type.INT_TYPE);
        mg.push(42.42);
        mg.storeLocal(var);
        Label varLabel = mg.mark();
        mg.returnValue();
        Label endLabel = mg.mark();
        mg.visitLocalVariable("x", "D", null, varLabel, endLabel, var);
        mg.endMethod();
        cw.visitEnd();
        byte[] bytes = cw.toByteArray();
        Files.write(Paths.get(name + ".class"), bytes);
    }

}

I am using a GeneratorAdapter to simplify code generation. Since GeneratorAdapter is inherited from LocalVariablesSorter, I assume that it is allowed to use newLocal(Type) method from it.

There is nothing wrong with the emitted bytecode except the name of the variable. When visitLocalVariable() method is called, instead of assigning a name to the variable it creates a new one in the bytecode.

Emitted bytecode:

// class version 52.0 (52)
// access flags 0x1
public class com/test/Sub {
  // access flags 0x1
  public <init>()V
    ALOAD 0
    INVOKESPECIAL java/lang/Object.<init> ()V
    LDC 42.42
    DSTORE 1
   L0
    RETURN
   L1
    LOCALVARIABLE x D L0 L1 3
    MAXSTACK = 2
    MAXLOCALS = 5
}

I am using the same variable index provided by newLocal() call in visitLocalVariable(). However in the bytecode mapped index is 3 instead of 1. If the variable has a "shorter" type such as int then the index would be 2 and still not 1 as it should.

From my observations this happens because of the following. LocalVariablesSorter maintains a mapping from old variable indices to new ones. It also overrides method visitLocalVariable and before delegating a call down a visitor chain it computes a newIndex from the mapping. The newIndex is calculated via another private method remap(). This method checks whether the mapping already exists for a given variable and if not then a new mapping is created. The problem as I see it is that newLocal() method does not add anything to the mapping.

Also I can see from ASM sources that storeInsn() in GeneratorAdapter delegates visitVarInsn() call down the chain instead of calling the implementation of LocalVariablesSorter. Because it is in LocalVariablesSorter implementation the remap() method is called for the variable index and mapping is updated.

Therefore my question is how to use GeneratorAdapter so variables are named properly in emitted bytecode or how to combine GeneratorAdapter with LocalVariablesSorter in a chain so they work properly together?


Solution

  • Since the GeneratorAdapter extends LocalVariablesSorter, whose purpose is to adapt all visitor calls, all methods which are part of the visitor API get adapted, unlike the dedicated methods introduced by GeneratorAdapter. This design allows to insert new code into an existing method, where the old code gets reported through the visitor API.

    So the method visitLocalVariable, which is part of the visitor API, must be called on the target MethodVisitor, bypassing the LocalVariablesSorter:

    ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_FRAMES);
    String name = "com.test.Sub";
    cw.visit(Opcodes.V1_8, Opcodes.ACC_PUBLIC,
             name.replace('.', '/'), null, "java/lang/Object", null);
    Method ctor = Method.getMethod("void <init>()");
    MethodVisitor direct = cw.visitMethod(
             Opcodes.ACC_PUBLIC, ctor.getName(), ctor.getDescriptor(), null, null);
    GeneratorAdapter mg = new GeneratorAdapter(Opcodes.ACC_PUBLIC, ctor, direct);
    mg.visitCode();
    mg.loadThis();
    mg.invokeConstructor(Type.getType(Object.class), ctor);
    int var = mg.newLocal(Type.DOUBLE_TYPE);
    mg.push(42.42);
    mg.storeLocal(var);
    Label varLabel = mg.mark();
    mg.returnValue();
    Label endLabel = mg.mark();
    direct.visitLocalVariable("x", "D", null, varLabel, endLabel, var);
    mg.endMethod();
    cw.visitEnd();
    byte[] bytes = cw.toByteArray();
    Files.write(Paths.get(name + ".class"), bytes);
    

    Since this can be confusing, here the alternative directly working on the target MethodVisitor entirely without any convenience wrapper like GeneratorAdapter. It isn’t more complex, though it requires slightly more knowledge, however, it’s knowledge that developers should have anyway, when dealing with Java bytecode and class files…

    ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_FRAMES);
    String name = "com.test.Sub";
    String superClName = "java/lang/Object", ctorName = "<init>", ctorDesc = "()V";
    cw.visit(Opcodes.V1_8, Opcodes.ACC_PUBLIC, name.replace('.','/'), null, superClName, null);
    MethodVisitor direct = cw.visitMethod(Opcodes.ACC_PUBLIC, ctorName, ctorDesc, null, null);
    direct.visitCode();
    // "this" is alway 0 (zero) and for parameterless methods the next var location is 1 (one)
    int thisVar = 0, var = 1;
    direct.visitVarInsn(Opcodes.ALOAD, thisVar);
    direct.visitMethodInsn(Opcodes.INVOKESPECIAL, superClName, ctorName, ctorDesc, false);
    direct.visitLdcInsn(42.42);
    Label varLabel = new Label(), endLabel = new Label();
    direct.visitVarInsn(Opcodes.DSTORE, var);
    direct.visitLabel(varLabel);
    direct.visitInsn(Opcodes.RETURN);
    direct.visitLabel(endLabel);
    direct.visitLocalVariable("x", "D", null, varLabel, endLabel, var);
    direct.visitMaxs(-1, -1);// no actual values, using COMPUTE_FRAMES
    direct.visitEnd();
    cw.visitEnd();
    byte[] bytes = cw.toByteArray();
    Files.write(Paths.get(name + ".class"), bytes);
    

    If you don’t feel comfortable with using ()V for a parameterless void method directly, you could still use the Method object like before or Type.getMethodDescriptor(Type.VOID_TYPE)