Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Implement duck typing using java MethodHandles

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.

like image 375
Netherwire Avatar asked Jun 11 '20 09:06

Netherwire


1 Answers

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");
    }
}
like image 81
Jorn Vernee Avatar answered Oct 11 '22 00:10

Jorn Vernee