6

I'm trying to replace a reflective invocation with a MethodHandle, but varargs seem to be impossible to deal with.

My reflective invoker currently looks like this:

public class Invoker {

    private final Method delegate;

    public Invoker(Method delegate) {
        this.delegate = delegate;
    }

    public Object execute(Object target, Object[] args) {
        return delegate.invoke(target, args);
    }
}

My current attempt at rewriting it looks like this (the interface the Invoker exposes has to stay the same):

public class Invoker {

    private final Method delegate;
    private final MethodHandle handle;

    public Invoker(Method delegate) {
        this.delegate = delegate;
        this.handle = MethodHandles.lookup().unreflect(delegate);
    }

    public Object execute(Object target, Object[] args) throws InvocationTargetException, IllegalAccessException {
        Object[] allArgs = Stream.concat(Stream.of(target), Stream.of(args)).toArray(Object[]::new);
        return handle.invokeWithArguments(allArgs);
    }
}

And this works just fine in most cases. But varargs break everything. E.g. have a method like:

public String test(int i, String... args) {
    return ...;
}

And the arguments like:

Object[] args = new Object[] {10, new String[] {"aaa", "bbb"}};

And execute as implemented above will fail. I tried various combinations of asSpreader(), MethodHandles.explicitCastArguments(), invoke instead of invokeWithArguments etc with no success.

The only way I can invoke a varargs method is to provide the arguments inline and not as an array. E.g.

handle.invokeWithArguments(10, "aaa", "bbb")

but I can not do that and maintain the generic nature of the Invoker that it currently has.

Is this really impossible to do the way I'm trying?

UPDATE: After benchmarking various scenarios, I decided to stick to reflection as invokeWithArguments performs significantly worse in all tested cases.

kaqqao
  • 12,984
  • 10
  • 64
  • 118
  • 1
    Did you try invokeExact with array in place of varargs argument? – GotoFinal Aug 30 '18 at 09:33
  • @GotoFinal Holy crap, I did not, and it works! But... I do need the autoboxing to keep working... Aaargh. – kaqqao Aug 30 '18 at 10:59
  • 1
    Note that instead of `Object[] allArgs = Stream.concat(Stream.of(target), Stream.of(args)).toArray(Object[]::new); return handle.invokeWithArguments(allArgs);`, you can simply use `return handle.bindTo(target).invokeWithArguments(args);` – Holger Aug 31 '18 at 09:18

2 Answers2

6

Seems that all you need is one call to .asFixedArity as by default java will create method handle with asVarargsCollector

public class Main {
    public static String test(int i, String... args) { return "works!"; }

    public static void main(String[] args) throws Throwable {
        Method method = Main.class.getMethod("test", int.class, String[].class);
        System.out.println(new Invoker(method).execute(null, new Object[]{1, new String[] {"foo", "bar"} }));
    }

    public static class Invoker {
        private final MethodHandle handle;

        public Invoker(final Method delegate) throws Exception {
            MethodHandle handle = MethodHandles.lookup().unreflect(delegate);
            if (Modifier.isStatic(delegate.getModifiers())) { // for easy static methods support
                handle = MethodHandles.dropArguments(handle, 0, Object.class);
            }
            this.handle = handle.asFixedArity();
        }

        public Object execute(Object target, Object[] args) throws Throwable {
            Object[] allArgs = new Object[args.length + 1];
            allArgs[0] = target;
            System.arraycopy(args, 0, allArgs, 1, args.length);
            return handle.invokeWithArguments(allArgs);
        }
    }
}

There is also many other possible solutions, like you can add more logic to Invoker constructor (static factory might be a good idea) and use asType method to prepare signature you want and then you should be able to call this using .invokeExact to get small performance boost.

You can also just keep using Method ;)

GotoFinal
  • 3,585
  • 2
  • 18
  • 33
  • 1
    @kaqqao why you added `bindTo` in execute? This will reduce performance a lot as you will make that handle fully dynamic so JIT will never be able to optimize this sitecall. – GotoFinal Sep 03 '18 at 09:41
  • And this just isn't valid way of using method handles, they are supposed to be created once for all future usages, otherwise they will be much slower, like around x15 here. – GotoFinal Sep 03 '18 at 09:59
  • Didn't realize the implications. Thanks again! – kaqqao Sep 03 '18 at 10:00
  • The strange thing here is why wouldn't `bindTo` just do the same argument prepending instead of something 15 times slower? – kaqqao Sep 03 '18 at 11:27
  • @kaqqao Each operation on MethodHandle that need to modify it will construct new MethodHandle, in this case the difference will be probably smaller as arraycopy will probably also eat some time, but for sure it would still be like 10x slower. (I only did benchmark for base version, without array copy call) – GotoFinal Sep 03 '18 at 12:08
  • In my benchmarks, `invokeWithArguments` (no `bindTo`) is performing significantly worse than `Method`. Any clue if this is normal? – kaqqao Sep 04 '18 at 19:44
  • @kaqqao it is possible, but hard to say if your benchmark is valid too. https://blog.gotofinal.com/java/benchmark/performance/2017/09/17/performance-of-java-3.html otherwise both reflections and method handles can be pretty fast. – GotoFinal Sep 04 '18 at 23:24
0

I completed your code to reproduce your issue, but it works using invokeWithArguments. Maybe I missed something ?

public class Main {

    public String test(int i, String... args) {
        return i + Stream.of(args).collect(Collectors.joining());
    }


    public static void main(String[] args) throws Throwable {
        Main main = new Main();
        Method method = Main.class.getMethod(
            "test",
            int.class,
            String[].class)
        Invoker invoker = new Invoker(method);
        assertEquals("1foobar", invoker.execute(main, new Object[]{1, "foo", "bar"})); // Success
    }


    public static class Invoker {

        private final MethodHandle handle;

        public Invoker(final Method delegate) throws Exception {
            this.handle = MethodHandles.lookup().unreflect(delegate);
        }

        public Object execute(Object target, Object[] args) throws Throwable {
            // Add the target and all arguments in a new array
            Object[] allArgs = Stream.concat(Stream.of(new Object[]{target}), Stream.of(args))
                .toArray(Object[]::new);
            return handle.invokeWithArguments(allArgs);
        }
    }
}
Junior Dussouillez
  • 2,327
  • 3
  • 30
  • 39
  • 1
    OP probably have String[] as argument, but I guess he could just check if method is varargs and unpack it then. – GotoFinal Aug 30 '18 at 12:12
  • Exactly as @GotoFinal said, when I receive the argument array, varargs won't be spread, but an array on their own. As if you had `new Object[]{1, new String[] {"foo", "bar"}}`. Exactly in the form you'd use for a regular reflective call. That's why I was trying with `asSpreader `. – kaqqao Aug 30 '18 at 15:10