1

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

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

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

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

如何构造一个正确的 MethodHandle,它像常规方法一样接受参数,然后调用 varargcallXXX方法?

4

1 回答 1

6

包文档中,我们找到了声明

调用站点目标的类型必须完全等于从调用的类型描述符派生并传递给引导方法的类型。

所以说兼容还不够,还得invoke兼容invokeExact

应用后.asVarargsCollector(Object[].class),可以到invoke句柄,但它与确切的签名不匹配。但我们可以通过以下方式对其进行调整asType

如果当前方法是可变的 arity 方法句柄参数列表转换,则可能涉及将多个参数转换和收集到一个数组中,如别处所述

这意味着 和 的组合asVarargsCollector应该asType起作用。但我们也可以考虑同一个方法文档中提到的invoke和之间的一般关系:invokeExact

invokeExact这种方法提供了和plain、inexact之间的关键行为差异invoke。当调用者的类型描述符与被调用者的类型完全匹配时,这两个方法执行相同的步骤,但是当类型不同时,plaininvoke也会调用asType(或一些内部等效项)以匹配调用者和被调用者的类型。

换句话说,如果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)使用该类型来应用。

于 2021-04-13T09:12:33.630 回答