javajvmmethodhandlevarhandle

Can't load hidden class with VarHandle


I am trying to load a class as a hidden class in Java, but run into a VerifyError. The class uses a VarHandle, which has a PolymorphicSignature. I believe the error is implying that the polymorphic call is using the non-hidden class, but it's not obvious to me what to do to fix it. What is the correct way to define hidden classes that contain calls to MethodHandles or VarHandles?

package foo;

import java.lang.invoke.MethodHandles;
import java.lang.invoke.VarHandle;
import java.util.function.IntUnaryOperator;
import org.junit.jupiter.api.Test;

public class HiddenClassTest {

  @Test
  public void loadHidden() throws Exception {
    MethodHandles.Lookup lookup;
    try (var is = IntThing.class.getResourceAsStream("HiddenClassTest$IntThing.class")) {
      lookup =
          MethodHandles.lookup().defineHiddenClass(is.readAllBytes(), true);
    }
    var instance =
        lookup.lookupClass()
            .asSubclass(IntUnaryOperator.class)
            .getConstructor()
            .newInstance();
    System.out.println(instance.applyAsInt(12));
    System.out.println(instance.applyAsInt(24));
  }

  static final class IntThing implements IntUnaryOperator {
    static final VarHandle IDX;

    static {
      try {
        IDX = MethodHandles.lookup().findVarHandle(IntThing.class, "idx", int.class);
      } catch (Exception e) {
        throw new RuntimeException(e);
      }
    }

    private int idx;

    @Override
    public int applyAsInt(int newIdx) {
      int oldIdx = (int) IDX.getOpaque(this);
      IDX.setOpaque(this, newIdx);
      return oldIdx;
    }
  }
}

The error:

Bad type on operand stack
Exception Details:
  Location:
    foo/HiddenClassTest$IntThing+0x0000000800d38c00.applyAsInt(I)I @4: invokevirtual
  Reason:
    Type 'foo/HiddenClassTest$IntThing+0x0000000800d38c00' (current frame, stack[1]) is not assignable to 'foo/HiddenClassTest$IntThing'
  Current Frame:
    bci: @4
    flags: { }
    locals: { 'foo/HiddenClassTest$IntThing+0x0000000800d38c00', integer }
    stack: { 'java/lang/invoke/VarHandle', 'foo/HiddenClassTest$IntThing+0x0000000800d38c00' }
  Bytecode:
    0000000: b200 072a b600 0d3d b200 072a 1bb6 0013
    0000010: 1cac                                   

java.lang.VerifyError: Bad type on operand stack
Exception Details:
  Location:
    foo/HiddenClassTest$IntThing+0x0000000800d38c00.applyAsInt(I)I @4: invokevirtual
  Reason:
    Type 'foo/HiddenClassTest$IntThing+0x0000000800d38c00' (current frame, stack[1]) is not assignable to 'foo/HiddenClassTest$IntThing'
  Current Frame:
    bci: @4
    flags: { }
    locals: { 'foo/HiddenClassTest$IntThing+0x0000000800d38c00', integer }
    stack: { 'java/lang/invoke/VarHandle', 'foo/HiddenClassTest$IntThing+0x0000000800d38c00' }
  Bytecode:
    0000000: b200 072a b600 0d3d b200 072a 1bb6 0013
    0000010: 1cac                                   

    at java.base/java.lang.ClassLoader.defineClass0(Native Method)
    at java.base/java.lang.System$2.defineClass(System.java:2307)
    at java.base/java.lang.invoke.MethodHandles$Lookup$ClassDefiner.defineClass(MethodHandles.java:2439)
    at java.base/java.lang.invoke.MethodHandles$Lookup$ClassDefiner.defineClassAsLookup(MethodHandles.java:2420)
    at java.base/java.lang.invoke.MethodHandles$Lookup.defineHiddenClass(MethodHandles.java:2127)
    at foo.HiddenClassTest.loadHidden(HiddenClassTest.java:16)
    at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:77)
    at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
    at java.base/java.lang.reflect.Method.invoke(Method.java:568)
    at org.junit.platform.commons.util.ReflectionUtils.invokeMethod(ReflectionUtils.java:725)
    at org.junit.jupiter.engine.execution.MethodInvocation.proceed(MethodInvocation.java:60)

Solution

  • You are right: this is because of the polymorphic signature where this results in a reference to a different class in the signature. The workaround is to explicitly cast this to Object to force Ljava/lang/Object; in the signature instead of LHiddenClassTest$IntThing;

    int oldIdx = (int) IDX.getOpaque((Object) this);
    IDX.setOpaque((Object) this, newIdx);
    return oldIdx;