JNI Invocation API

Native Image 可用于在 Java 中实现低级系统操作,并通过 JNI 调用 API 将其提供给在标准 JVM 上执行的 Java 代码。因此,可以使用同一种语言编写应用程序逻辑和系统调用。

请注意,本文档描述的与通常通过 JNI 完成的方式相反:通常,低级系统操作是用 C 实现的,并使用 JNI 从 Java 调用。如果您有兴趣了解 Native Image 如何支持常见用例,请参阅 Native Image 中的 Java 本地接口 (JNI)

创建共享库 #

首先,必须使用 native-image 构建器生成具有 JNI 兼容入口点的共享库。从 Java 代码开始

package org.pkg.implnative;

import org.graalvm.nativeimage.c.function.CEntryPoint;
import org.graalvm.word.Pointer;

public final class NativeImpl {
    @CEntryPoint(name = "Java_org_pkg_apinative_Native_add")
    public static int add(Pointer jniEnv, Pointer clazz, @CEntryPoint.IsolateThreadContext long isolateId, int a, int b) {
        return a + b;
    }
}

经过 native-image 构建器处理后,代码 公开了一个 C 函数 Java_org_pkg_apinative_Native_add(此名称遵循 JNI 约定,稍后将派上用场)以及 JNI 方法典型的 Native Image 签名。第一个参数是对 JNIEnv* 值的引用。第二个参数是对声明该方法的类的 jclass 值的引用。第三个参数是 Native Image 隔离线程 的可移植(例如,long 类型)标识符。其余参数是下一节中描述的 Java Native.add 方法的实际参数。使用 --shared 选项编译代码

$JAVA_HOME/bin/native-image --shared -H:Name=libnativeimpl -cp nativeimpl

生成了 libnativeimpl.so。我们可以从标准 Java 代码中使用它了。

绑定 Java 本地方法 #

现在我们需要另一个 Java 类来使用上一步生成的本地库

package org.pkg.apinative;

public final class Native {
    private static native int add(long isolateThreadId, int a, int b);
}

类的包名以及方法名必须(在 JNI 名称修饰后)与之前介绍的 @CEntryPoint 的名称相对应。第一个参数是 Native Image 隔离线程的可移植(例如,long 类型)标识符。其余参数与入口点的参数匹配。

加载本地库 #

下一步是将 JDK 与生成的 .so 库绑定。例如,确保本地 Native.add 方法的实现已加载。简单的 loadloadLibrary 调用即可。

public static void main(String[] args) {
    System.loadLibrary("nativeimpl");
    // ...
}

这是假设您的 LD_LIBRARY_PATH 环境变量已指定,或者 java.library.path Java 属性已正确设置。

初始化 Native Image 隔离区 #

在调用 Native.add 方法之前,我们需要创建一个 Native Image 隔离区。Native Image 提供了一个特殊的内置功能来允许这样做:CEntryPoint.Builtin.CREATE_ISOLATE。在您其他现有的 @CEntryPoint 方法旁边定义另一个方法。让它返回 IsolateThread 且不带任何参数

public final class NativeImpl {
    @CEntryPoint(name = "Java_org_pkg_apinative_Native_createIsolate", builtin=CEntryPoint.Builtin.CREATE_ISOLATE)
    public static native IsolateThread createIsolate();
}

Native Image 然后将该方法的默认本地实现生成到最终的 .so 库中。该方法初始化 Native Image 运行时,并返回一个可移植标识符(例如,long 类型),以保存 Native Image 隔离线程 的实例。然后该隔离线程可用于多次调用代码的本地部分

package org.pkg.apinative;

public final class Native {
    public static void main(String[] args) {
        System.loadLibrary("nativeimpl");

        long isolateThread = createIsolate();

        System.out.println("2 + 40 = " + add(isolateThread, 2, 40));
        System.out.println("12 + 30 = " + add(isolateThread, 12, 30));
        System.out.println("20 + 22 = " + add(isolateThread, 20, 22));
    }

    private static native int add(long isolateThread, int a, int b);
    private static native long createIsolate();
}

标准 JVM 已启动。它初始化了一个 Native Image 隔离区,将当前线程附加到该隔离区,然后在隔离区内计算通用答案 42 三次。

从本地 Java 调用 JVM #

有关 Native Image C 接口的详细教程。以下示例展示了如何向 JVM 进行回调。

在经典设置中,当 C 需要调用 JVM 时,它使用一个 jni.h 头文件。该文件定义了基本的 JVM 结构(如 JNIEnv)以及可以在 JVM 中调用以检查类、访问字段和调用方法的功能。为了在上述示例中从 NativeImpl 类调用这些函数,您需要定义 jni.h 概念的相应 Java API 包装器

@CContext(JNIHeaderDirectives.class)
@CStruct(value = "JNIEnv_", addStructKeyword = true)
interface JNIEnvironment extends PointerBase {
    @CField("functions")
    JNINativeInterface getFunctions();
}

@CPointerTo(JNIEnvironment.class)
interface JNIEnvironmentPointer extends PointerBase {
    JNIEnvironment read();
    void write(JNIEnvironment value);
}

@CContext(JNIHeaderDirectives.class)
@CStruct(value = "JNINativeInterface_", addStructKeyword = true)
interface JNINativeInterface extends PointerBase {
    @CField
    GetMethodId getGetStaticMethodID();

    @CField
    CallStaticVoidMethod getCallStaticVoidMethodA();
}

interface GetMethodId extends CFunctionPointer {
    @InvokeCFunctionPointer
    JMethodID find(JNIEnvironment env, JClass clazz, CCharPointer name, CCharPointer sig);
}

interface JObject extends PointerBase {
}

interface CallStaticVoidMethod extends CFunctionPointer {
    @InvokeCFunctionPointer
    void call(JNIEnvironment env, JClass cls, JMethodID methodid, JValue args);
}

interface JClass extends PointerBase {
}
interface JMethodID extends PointerBase {
}

暂且不谈 JNIHeaderDirectives 的含义,其余接口是 jni.h 文件中 C 指针的类型安全表示。JClassJMethodIDJObject 都是指针。由于上述定义,您现在拥有 Java 接口,可以以类型安全的方式在您的本地 Java 代码中表示这些对象的实例。

任何 JNI API 的核心部分是与 JVM 通信时可以调用的一组函数。它们有几十个,但在 JNINativeInterface 定义中,您只需为示例中需要的少数几个定义包装器。同样,给它们正确的类型,以便在您的本地 Java 代码中可以使用 GetMethodId.find(...)CallStaticVoidMethod.call(...) 等。此外,这个难题中还缺少另一个重要部分——jvalue 联合类型,它包装了所有可能的 Java 原始类型和对象类型。以下是其 getter 和 setter 的定义

@CContext(JNIHeaderDirectives.class)
@CStruct("jvalue")
interface JValue extends PointerBase {
    @CField boolean z();
    @CField byte b();
    @CField char c();
    @CField short s();
    @CField int i();
    @CField long j();
    @CField float f();
    @CField double d();
    @CField JObject l();


    @CField void z(boolean b);
    @CField void b(byte b);
    @CField void c(char ch);
    @CField void s(short s);
    @CField void i(int i);
    @CField void j(long l);
    @CField void f(float f);
    @CField void d(double d);
    @CField void l(JObject obj);

    JValue addressOf(int index);
}

addressOf 方法是 Native Image 中用于执行 C 指针算术的特殊构造。给定一个指针,可以将其视为数组的初始元素,然后,例如,使用 addressOf(1) 访问后续元素。有了这些,您就拥有了进行回调所需的所有 API——重新定义先前引入的 NativeImpl.add 方法以接受正确类型的指针,然后在使用这些指针计算 a + b 的和之前调用 JVM 方法

@CEntryPoint(name = "Java_org_pkg_apinative_Native_add")
static int add(JNIEnvironment env, JClass clazz, @CEntryPoint.IsolateThreadContext long isolateThreadId, int a, int b) {
    JNINativeInterface fn = env.getFunctions();

    try (
        CTypeConversion.CCharPointerHolder name = CTypeConversion.toCString("hello");
        CTypeConversion.CCharPointerHolder sig = CTypeConversion.toCString("(ZCBSIJFD)V");
    ) {
        JMethodID helloId = fn.getGetStaticMethodID().find(env, clazz, name.get(), sig.get());

        JValue args = StackValue.get(8, JValue.class);
        args.addressOf(0).z(false);
        args.addressOf(1).c('A');
        args.addressOf(2).b((byte)22);
        args.addressOf(3).s((short)33);
        args.addressOf(4).i(39);
        args.addressOf(5).j(Long.MAX_VALUE / 2l);
        args.addressOf(6).f((float) Math.PI);
        args.addressOf(7).d(Math.PI);
        fn.getCallStaticVoidMethodA().call(env, clazz, helloId, args);
    }

    return a + b;
}

上述示例查找静态方法 hello,并使用 StackValue.get 在栈上保留的数组中的八个 JValue 参数调用它。在调用发生之前,通过使用 addressOf 运算符访问各个参数并填充适当的原始值。方法 helloNative 类中定义,并打印所有参数的值以验证它们是否已从 NativeImpl.add 调用方正确传递。

public class Native {
    public static void hello(boolean z, char c, byte b, short s, int i, long j, float f, double d) {
        System.err.println("Hi, I have just been called back!");
        System.err.print("With: " + z + " " + c + " " + b + " " + s);
        System.err.println(" and: " + i + " " + j + " " + f + " " + d);
    }

还有最后一部分需要解释:JNIHeaderDirectives。Native Image C 接口需要理解 C 结构的布局。它需要知道在 JNINativeInterface 结构的哪个偏移量可以找到指向 GetMethodId 函数的指针。为此,它在编译期间需要 jni.h 和附加文件。可以通过 @CContext 注解及其 Directives 的实现来指定它们

final class JNIHeaderDirectives implements CContext.Directives {
    @Override
    public List<String> getOptions() {
        File[] jnis = findJNIHeaders();
        return Arrays.asList("-I" + jnis[0].getParent(), "-I" + jnis[1].getParent());
    }

    @Override
    public List<String> getHeaderFiles() {
        File[] jnis = findJNIHeaders();
        return Arrays.asList("<" + jnis[0] + ">", "<" + jnis[1] + ">");
    }

    private static File[] findJNIHeaders() throws IllegalStateException {
        final File jreHome = new File(System.getProperty("java.home"));
        final File include = new File(jreHome.getParentFile(), "include");
        final File[] jnis = {
            new File(include, "jni.h"),
            new File(new File(include, "linux"), "jni_md.h"),
        };
        return jnis;
    }
}

好消息是 jni.h 位于每个 JDK 中,因此可以使用 java.home 属性来定位必要的头文件。当然,实际逻辑可以做得更健壮且与操作系统无关。

现在,在 Java 中实现任何 JVM 本地方法和/或使用 Native Image 向 JVM 进行回调,都应该像在给定示例的基础上进行扩展并调用 native-image 一样简单。

联系我们