从 invokedynamic 调用 Java 可变参数方法

Call Java varargs method from invokedynamic

我想从 Java 动态调用本机方法。 因为方法签名在编译时是未知的,所以我为大多数具有相同签名的原始 return 类型制作了通用本地方法:

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));
    }
}

我想创建一个 MethodHandle,然后调用匹配的 callXXX 方法并传入盒装 funcArgs,就好像它们是单独提供的一样。这些 callXXX 方法可以像这样访问:

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);

我正在使用这个bootstrap方法间接引用invokedynamic中的这个callXXX方法,它的工作方式与上面相同:

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));
    }
}

然后使用 invokedynamic 完成调用,如下所示:

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

然而,这并没有像预期的那样工作并抛出异常:

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

如何构造一个像常规方法一样接受参数但随后调用可变参数 callXXX 方法的正确 MethodHandle?

the package documentation中我们找到语句

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.

所以只兼容invoke还不够,还得兼容invokeExact.

应用.asVarargsCollector(Object[].class)后,可以invoke句柄,但不匹配确切的签名。但是我们可以通过 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.

这意味着 asVarargsCollectorasType 的组合应该有效。但是我们也可以考虑一下同方法文档中提到的invokeinvokeExact的一般关系:

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.

换句话说,如果invoke成功,asType转换也必须能够满足invokeExact的要求。

我们可以证明:

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

bootstrap 方法确实已经接收到所需的 MethodType 作为第三个参数,因此它需要做的就是使用该类型应用 .asType(type)