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?
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, inexactinvoke
. The two methods perform the same steps when the caller's type descriptor exactly matches the callee's, but when the types differ, plaininvoke
also callsasType
(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.