Implement duck typing using java MethodHandles
Asked Answered
K

1

5

I have two classes A and B, both define method foo() with common signature (accept nothing, return void). They don't have the common base class (or interface) that declares this method. I want to call this method on regardless As or Bs as long as they can respond to this call. This approach is called Duck Typing.

I know that there's an instruction called invokedynamic:

Each instance of an invokedynamic instruction is called a dynamic call site. A dynamic call site is originally in an unlinked state, which means that there is no method specified for the call site to invoke. As previously mentioned, a dynamic call site is linked to a method by means of a bootstrap method. A dynamic call site's bootstrap method is a method specified by the compiler for the dynamically-typed language that is called once by the JVM to link the site. The object returned from the bootstrap method permanently determines the call site's behavior.

So I tried to achive this using MethodHandles. Here's the example:

public static class A {
    public void foo() {
    }
}

public static class B {
    public void foo() {
    }
}

public static void main(String[] args) throws Throwable {
    final MethodHandle foo = MethodHandles.lookup()
            .findVirtual(A.class, "foo", MethodType.methodType(void.class));

    foo.invoke(new B());
}

Of course, I've got:

Exception in thread "main" java.lang.ClassCastException: Cannot cast Main$B to Main$A
    at sun.invoke.util.ValueConversions.newClassCastException(ValueConversions.java:461)
    at sun.invoke.util.ValueConversions.castReference(ValueConversions.java:456)
    at Main.main(Main.java:30)

I clearly see the difference between invokedynamic and MethodHanle. I see that the problem is that the foo MethodHandle is bound to the class A, not class B. But is it possible for me to somehow take advantage of invokedynamic in this particular case?

Why do I need this? This is the part of my small research project. I'm trying to understand method handles in depth and I want to call common methods on annotation instances retrieved from fields and methods. I am unable to define base class for annotations in Java, so instead of chain of instanceof's and class casts or retrieving these values using reflection violating access rights, I want to implement this duck typing if possible.

Thanks.

Kai answered 11/6, 2020 at 9:32 Comment(1)
Your current problem is not about invokedynamic or anything, you need an instance of the correct class to call instance methods. Of course you can't call A methods on B objects as A implementation of foo() may depend on fields / data / private function only present in A. invokedynamic needs an instance too. On static methods you can pass null as the this parameters to invoke, but really: duck typing only works if you already have an instance of the object, which you are missing atm :)Byrom
T
7

When the VM encounters an invokedynamic instruction for the first time it calls a factory method, or 'bootstrap' method, that returns a CallSite object who's target implements the actual functionality. You can implement that yourself using a MutableCallSite that looks up your target method on first invocation and then sets it's own target to the looked up method.

But, this is not enough for your purposes. You want to re-link the call site when you encounter a new receiver type.

Here is an example (that currently only supports findVirtual):

class DuckTypingCallSite extends MutableCallSite {

    private static final MethodHandle MH_relink;
    private static final MethodHandle MH_isInstance;

    static {
        try {
            MH_relink = lookup().findVirtual(DuckTypingCallSite.class, "link", methodType(Object.class, Object[].class));
            MH_isInstance = lookup().findVirtual(Class.class, "isInstance", methodType(boolean.class, Object.class));
        } catch (ReflectiveOperationException e) {
            throw new InternalError(e);
        }
    }

    private final MethodHandles.Lookup lookup;
    private final String methodName;
    private final MethodType lookupType;

    private DuckTypingCallSite(MethodHandles.Lookup lookup, String methodName, MethodType lookupType) {
        super(lookupType.insertParameterTypes(0, Object.class)); // insert receiver
        this.lookup = lookup;
        this.methodName = methodName;
        this.lookupType = lookupType;
    }

    public static DuckTypingCallSite make(MethodHandles.Lookup lookup, String methodName, MethodType lookupType) {
        DuckTypingCallSite cs = new DuckTypingCallSite(lookup, methodName, lookupType);
        cs.setTarget(MH_relink.bindTo(cs).asCollector(Object[].class, cs.type().parameterCount()).asType(cs.type()));
        return cs;
    }

    public Object link(Object[] args) throws Throwable {
        Object receiver = args[0];
        Class<?> holder = receiver.getClass();
        MethodHandle target = lookup.findVirtual(holder, methodName, lookupType).asType(type());

        MethodHandle test = MH_isInstance.bindTo(holder);
        MethodHandle newTarget = guardWithTest(test, target, getTarget());
        setTarget(newTarget);

        return target.invokeWithArguments(args);
    }

}

Before the first invocation, calling the call site's dynamic invoker will jump right into the link method, which will lookup the target method and then invoke that, as well as re-linking the DuckTypingCallSite to basically cache the looked up MethodHandle, guarded by a type check.

After the first invocation, this essentially creates an if/else like this:

if (A.class.isInstance(receiver)) {
    // invoke A.foo
} else {
    // re-link
}

Then when the second type is encountered it changes to this:

if (B.class.isInstance(receiver)) {
    // invoke B.foo
} else if (A.class.isInstance(receiver)) {
    // invoke A.foo
} else {
    // re-link
}

etc.

And here is an example usage:

public class DuckTyping {

    private static final MethodHandle MH_foo = DuckTypingCallSite.make(lookup(), "foo", methodType(void.class)).dynamicInvoker();

    private static void foo(Object receiver) {
        try {
            MH_foo.invokeExact(receiver);
        } catch (Throwable throwable) {
            throw new IllegalStateException(throwable);
        }
    }

    public static void main(String[] args) {
        foo(new A()); // prints "A.foo"
        foo(new B()); // prints "B.foo"
    }
}

class A {
    public void foo() {
        System.out.println("A.foo");
    }
}

class B {
    public void foo() {
        System.out.println("B.foo");
    }
}
Tint answered 11/6, 2020 at 11:50 Comment(2)
Thank you, it works just fine! But how fast is it? I see that relink() is only being called once per each new type. You've shown this as the chain of ifs in the pseudo-code, but what really does happen? Thank you again.Kai
@Kai The chain of ifs is pretty much what happens. After a while the MethodHandle chain will be rendered to bytecode, which will be optimized like normal. If you're interested in the internal implementation, I recommend checking out the some of the JVMLS talks about it: Lambda Forms & State of java.lang.invokeTint

© 2022 - 2024 — McMap. All rights reserved.