javajvmbytecodejava-bytecode-asminvokedynamic

Call Java varargs method from invokedynamic


I want to dynamically call a native method from Java. Because the method signature is unknown at compile time, I've made generic native methods for most primitive return types that have the same signature:

class NativeHook {
    
    public static native int callInt(String funcName, Object... funcArgs);
    public static native void callVoid(String funcName, Object... funcArgs);
    public static native Object callObject(String funcName, Object... funcArgs);

    private static MethodHandle getNativeMethod(String callName, Class<?> returnType) {
        return MethodHandles.lookup().findStatic(NativeHook.class, callName,
            MethodType.methodType(returnType, String.class, Object[].class));
    }
}

I'm looking to create a MethodHandle that would then call a matching callXXX method and pass in the boxed funcArgs as if they were provided individually. These callXXX methods can be accessed like this:

MethodHandle callInt = getNativeMethod("callInt", int.class);
MethodHandle boundCallInt = callInt.bindTo("my_c_function_name").asVarargsCollector(Object[].class);

// returns NativeHook.callInt("my_c_function_name", 1, 2, 3)
boundCallInt.invokeWithArguments(1, 2, 3);

I'm using this bootstrap method to indirectly reference this callXXX method in invokedynamic, which works the same way as above:

public static CallSite bootstrap(MethodHandles.Lookup caller, String name, MethodType type) {
    if (type.returnType() == int.class) {
        MethodHandle callInt = getNativeMethod("callInt", int.class);
        return new ConstantCallSite(callInt.bindTo(name).asVarargsCollector(Object[].class));
    }
}

The call is then done with invokedynamic like this:

mv.visitIntInsn(BIPUSH, 1);
mv.visitIntInsn(BIPUSH, 2);
mv.visitIntInsn(BIPUSH, 3);
mv.visitInvokeDynamicInsn("my_c_function_name", "(III)I", NativeHook.bootstrapHandle);

However, this does not work as expected and throws an exception:

Caused by: java.lang.invoke.WrongMethodTypeException: MethodHandle(Object[])int should be of type (int,int,int)int
    at java.lang.invoke.CallSite.wrongTargetType(CallSite.java:194)
    at java.lang.invoke.CallSite.makeSite(CallSite.java:335)
    ... 16 more

How do I construct a proper MethodHandle that accepts arguments like a regular method but then calls the vararg callXXX method?


Solution

  • In the package documentation we find the statement

    The type of the call site's target must be exactly equal to the type derived from the invocation's type descriptor and passed to the bootstrap method.

    So it is not enough to be compatible in terms of invoke, but it has to be compatible with invokeExact.

    After applying .asVarargsCollector(Object[].class), it is possible to invoke the handle, but it’s not matching the exact signature. But we can adapt it via asType:

    If the current method is a variable arity method handle argument list conversion may involve the conversion and collection of several arguments into an array, as described elsewhere.

    This implies that the combination of asVarargsCollector and asType should work. But we can also consider the general relationship between invoke and invokeExact mentioned in the same method documentation:

    This method provides the crucial behavioral difference between invokeExact and plain, inexact invoke. The two methods perform the same steps when the caller's type descriptor exactly matches the callee's, but when the types differ, plain invoke also calls asType (or some internal equivalent) in order to match up the caller's and callee's types.

    In other words, if invoke works successfully, the asType conversion also must be possible to meet the requirements for invokeExact.

    Which we can demonstrate:

    MethodHandles.Lookup l = MethodHandles.lookup();
    MethodHandle h = l.bind(System.out, "printf",
        MethodType.methodType(PrintStream.class, String.class, Object[].class));
    
    h = h.bindTo("%s %s %s%n").asVarargsCollector(Object[].class);
    
    try {
        System.out.println("invoke(1, 2, 3): ");
        h.invoke(1, 2, 3);
    } catch(Throwable t) {
        System.out.println(t);
    }
    try {
        System.out.println("\ninvokeExact(1, 2, 3): ");
        h.invokeExact(1, 2, 3);
    } catch(Throwable t) {
        System.out.println(t);
    }
    
    MethodType type = MethodType.methodType(void.class, int.class, int.class, int.class);
    
    try {
        System.out.println("\n.asType(type).invokeExact(1, 2, 3): ");
        h.asType(type).invokeExact(1, 2, 3);
    } catch(Throwable t) {
        System.out.println(t);
    }
    
    invoke(1, 2, 3): 
    1 2 3
    
    invokeExact(1, 2, 3): 
    java.lang.invoke.WrongMethodTypeException: expected (Object[])PrintStream but found (int,int,int)void
    
    .asType(type).invokeExact(1, 2, 3): 
    1 2 3
    

    The bootstrap method does receive the required MethodType as third argument already, so all it needs to do, it to apply .asType(type) using that type.