Why can't I .invokeExact() here, even though the MethodType is OK?

2.8k views Asked by At

For one of my project I have to make dynamic invocations of constructor. But since this is Java 7, instead of the "classic" reflection API, I use java.lang.invoke.

Code:

@ParametersAreNonnullByDefault
public class PathMatcherProvider
{
    private static final MethodHandles.Lookup LOOKUP
        = MethodHandles.publicLookup();
    private static final MethodType CONSTRUCTOR_TYPE
        = MethodType.methodType(void.class, String.class);

    private final Map<String, Class<? extends PathMatcher>> classMap
        = new HashMap<>();
    private final Map<Class<? extends PathMatcher>, MethodHandle> handleMap
        = new HashMap<>();

    public PathMatcherProvider()
    {
        registerPathMatcher("glob", GlobPathMatcher.class);
        registerPathMatcher("regex", RegexPathMatcher.class);
    }

    public final PathMatcher getPathMatcher(final String name, final String arg)
    {
        Objects.requireNonNull(name);
        Objects.requireNonNull(arg);

        final Class<? extends PathMatcher> c = classMap.get(name);
        if (c == null)
            throw new UnsupportedOperationException();

        try {
            return c.cast(handleMap.get(c).invoke(arg));
        } catch (Throwable throwable) {
            throw new RuntimeException("Unhandled exception", throwable);
        }
    }

    protected final void registerPathMatcher(@Nonnull final String name,
        @Nonnull final Class<? extends PathMatcher> matcherClass)
    {
        Objects.requireNonNull(name);
        Objects.requireNonNull(matcherClass);
        try {
            classMap.put(name, matcherClass);
            handleMap.put(matcherClass, findConstructor(matcherClass));
        } catch (NoSuchMethodException | IllegalAccessException e) {
            throw new RuntimeException("cannot find constructor", e);
        }
    }

    private static <T extends PathMatcher> MethodHandle findConstructor(
        final Class<T> matcherClass)
        throws NoSuchMethodException, IllegalAccessException
    {
        Objects.requireNonNull(matcherClass);
        return LOOKUP.findConstructor(matcherClass, CONSTRUCTOR_TYPE);
    }

    public static void main(final String... args)
    {
        new PathMatcherProvider().getPathMatcher("regex", "^a");
    }
}

OK, this works.

The problem I have is with this line:

return c.cast(handleMap.get(c).invoke(arg));

If I replace invoke with invokeExact, I get this stack trace:

Exception in thread "main" java.lang.RuntimeException: Unhandled exception
    at com.github.fge.filesystem.path.matchers.PathMatcherProvider.getPathMatcher(PathMatcherProvider.java:62)
    at com.github.fge.filesystem.path.matchers.PathMatcherProvider.main(PathMatcherProvider.java:89)
    at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:57)
    at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
    at java.lang.reflect.Method.invoke(Method.java:606)
    at com.intellij.rt.execution.application.AppMain.main(AppMain.java:134)
Caused by: java.lang.invoke.WrongMethodTypeException: expected (String)RegexPathMatcher but found (String)Object
    at java.lang.invoke.Invokers.newWrongMethodTypeException(Invokers.java:350)
    at java.lang.invoke.Invokers.checkExactType(Invokers.java:361)
    at com.github.fge.filesystem.path.matchers.PathMatcherProvider.getPathMatcher(PathMatcherProvider.java:60)

I don't quite get it. Both of GlobPathMatcher and RegexPathMatcher use a single constructor with a String as an argument, and the MethodType for both is therefore what is defined in CONSTRUCTOR_TYPE. If it weren't I couldn't have "grabbed" the MethodHandles anyway.

Yet I get a WrongMethodTypeException. Why?


EDIT: here is the code after I have read the answer; now I don't need the intermediate map: I just have to have one map, mapping a String to a MethodHandle:

@ParametersAreNonnullByDefault
public class PathMatcherProvider
{
    private static final MethodHandles.Lookup LOOKUP
        = MethodHandles.publicLookup();
    private static final MethodType CONSTRUCTOR_TYPE
        = MethodType.methodType(void.class, String.class);

    private final Map<String, MethodHandle> handleMap
        = new HashMap<>();

    public PathMatcherProvider()
    {
        registerPathMatcher("glob", GlobPathMatcher.class);
        registerPathMatcher("regex", RegexPathMatcher.class);
    }

    public final PathMatcher getPathMatcher(final String name, final String arg)
    {
        Objects.requireNonNull(name);
        Objects.requireNonNull(arg);

        final MethodHandle handle = handleMap.get(name);
        if (handle == null)
            throw new UnsupportedOperationException();

        try {
            return (PathMatcher) handle.invokeExact(arg);
        } catch (Throwable throwable) {
            throw new RuntimeException("Unhandled exception", throwable);
        }
    }

    protected final void registerPathMatcher(@Nonnull final String name,
        @Nonnull final Class<? extends PathMatcher> matcherClass)
    {
        Objects.requireNonNull(name);
        Objects.requireNonNull(matcherClass);

        final MethodHandle handle;
        final MethodType type;

        try {
            handle = LOOKUP.findConstructor(matcherClass, CONSTRUCTOR_TYPE);
            type = handle.type().changeReturnType(PathMatcher.class);
            handleMap.put(name, handle.asType(type));
        } catch (NoSuchMethodException | IllegalAccessException e) {
            throw new RuntimeException("cannot find constructor", e);
        }
    }
}
2

There are 2 answers

3
Jeffrey Bosboom On BEST ANSWER

When the compiler emits the invokeExact call, it records Object as the expected return type. From the MethodHandle javadoc (emphasis mine):

As is usual with virtual methods, source-level calls to invokeExact and invoke compile to an invokevirtual instruction. More unusually, the compiler must record the actual argument types, and may not perform method invocation conversions on the arguments. Instead, it must push them on the stack according to their own unconverted types. The method handle object itself is pushed on the stack before the arguments. The compiler then calls the method handle with a symbolic type descriptor which describes the argument and return types.

To issue a complete symbolic type descriptor, the compiler must also determine the return type. This is based on a cast on the method invocation expression, if there is one, or else Object if the invocation is an expression or else void if the invocation is a statement. The cast may be to a primitive type (but not void).

At runtime the method handle actually returns RegexPathMatcher, so the invokeExact fails with WrongMethodTypeException.

You need to specify the return type explicitly with a (compile-time) cast:

return (RegexPathMatcher)handleMap.get(c).invokeExact(arg);

Except you need to be generic over different PathMatcher implementations, so you should convert your method handles to return PathMatcher using asType, then call with PathMatcher as the expected return type.

//in findConstructor
MethodHandle h = LOOKUP.findConstructor(matcherClass, CONSTRUCTOR_TYPE);
return h.asType(h.type().changeReturnType(PathMatcher.class));

//in getPathMatcher
return (PathMatcher)handleMap.get(c).invokeExact(arg);
0
Eugene On

After 3 years of posting, I came to read this and while the answer is indeed correct, it was pretty hard to grasp everything. So, with all due respect, I'll post a slightly different approach (in case someone like me had to scratch his head twice to actually understand).

The main problem here is two different invocations : invoke and invokeExact. But first, these two methods in the source code are annotated with

@PolymorphicSignature

which are called compiler overloads also. These methods are treated very special by the java compiler - no other methods are treated the same way.

To understand let's provide an example. Here is a simple class with a single method:

static class Calle {

    public Object go(Object left, Object right) {
        // do something with left and right
        return new Object();
    }

}

Compile that and look at what the generated bytecode looks like (javap -c Calle.class). Among some lines there will be this method:

public java.lang.Object go(java.lang.Object, java.lang.Object);

The signature of it is: two arguments of type java.lang.Object and a return of type java.lang.Object. So far, so good.

So it's perfectly legal to do this:

 Calle c = new Calle();
 int left = 3;
 int right = 4;
 c.go(left, right);

And the bytecode for that will look:

invokevirtual #5 // Method CompilerOverloads$Calle.go:(Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object;

The method takes two Objects, and two integers are perfectly legal to be passed as parameters.

Now think about the definition of the method:

 MethodHandle#invoke

it's signature is java.lang.Object var arg and returns a java.lang.Object.

Thus how will this code compile?

 Lookup l = MethodHandles.lookup();
 MethodType type = MethodType.methodType(Object.class, Object.class, Object.class);
 MethodHandle handle = l.findVirtual(Calle.class, "go", type);
 Object result = handle.invoke(c, left, right); // what is generated here?

Interesting enough it compiles very different then our Calle::go

  Method java/lang/invoke/MethodHandle.invoke:(LCalle;II)Ljava/lang/Object;

It's input parameters are : Integer, Integer and return type is java.lang.Object. It's like the compiler trusted the compile time method declaration and generated the method signature out of that.

If we want to change the return type to be int for example, we need to specify that as a cast at compile time:

 int result = (int) handle.invoke(c, left, right); 

And then it signatures changes at the byte code level(emphasis is mine):

Method java/lang/invoke/MethodHandle.invoke:(LCalle;II)I

This does not happen anywhere else in jdk world as far as I know.

And now the problem of invoke vs invokeExact becomes a bit obvious (one is an exact signature and the other is a bit more loose).