2

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)
Carl Mastrangelo
  • 5,970
  • 1
  • 28
  • 37

2 Answers2

3

As you have found, a hidden class can not be referenced by name from byte code. It can only be referenced through the this_class constant pool entry. This excludes use from descriptor strings though.

As apangin says, you could explicitly cast the this argument to Object to avoid the class name from appearing in the descriptor.

If you want to avoid the inexact invocation that results from this, you could further convert the VarHandle into a set of MethodHandles for which the first parameter is erased to Object, and then invoke those with an exact call:

  static final class IntThing implements IntUnaryOperator {
    static final MethodHandle IDX_GET;
    static final MethodHandle IDX_SET;

    static {
      try {
        VarHandle VH_idx = MethodHandles.lookup().findVarHandle(IntThing.class, "idx", int.class);
        IDX_GET = VH_idx.toMethodHandle(VarHandle.AccessMode.GET_OPAQUE).asType(MethodType.methodType(int.class, Object.class));
        IDX_SET = VH_idx.toMethodHandle(VarHandle.AccessMode.SET_OPAQUE).asType(MethodType.methodType(void.class, Object.class, int.class));
      } catch (Exception e) {
        throw new RuntimeException(e);
      }
    }

    private int idx;
    
    private int getIdx() {
      try {
        return (int) IDX_GET.invokeExact((Object) this);
      } catch (Throwable t) {
        throw new RuntimeException(t);
      }
    }
    
    private void setIdx(int value) {
      try {
        IDX_SET.invokeExact((Object) this, value);
      } catch (Throwable t) {
        throw new RuntimeException(t);
      }
    }

    @Override
    public int applyAsInt(int newIdx) {
      int oldIdx = getIdx();
      setIdx(newIdx);
      return oldIdx;
    }
  }
Jorn Vernee
  • 31,735
  • 4
  • 76
  • 93
2

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;
apangin
  • 92,924
  • 10
  • 193
  • 247
  • Does this cause extra work for the VM? Phrased differently, are there any downsides to always casting to Object? – Carl Mastrangelo Mar 20 '23 at 23:48
  • @CarlMastrangelo Not in this case. There is a class cast inside `getOpaque` that verifies that the passed argument matches the receiver type of the VarHandle, but HotSpot is smart enough to recognize a redundant cast and eliminate it. – apangin Mar 21 '23 at 04:10