JNI 调用 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 isolatethread 的可移植(例如,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 isolatethread 的可移植(例如,long)标识符。其余参数与入口点的参数匹配。

加载原生库 #

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

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

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

初始化 Native Image Isolate #

在调用 Native.add 方法之前,我们需要创建一个 Native Image isolate。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 isolatethread 的实例。然后,可以使用 isolatethread 多次调用代码的原生部分

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 isolate,将当前线程附加到 isolate,然后在 isolate 内部三次计算通用答案 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 运算符访问各个参数,并在调用之前使用适当的基本值填充它们。方法 hello 定义在类 Native 中,并将所有参数的值打印出来以验证它们是否已从 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 一样简单。

联系我们