- 适用于 JDK 23 的 GraalVM(最新版本)
- 适用于 JDK 24 的 GraalVM(抢先体验版)
- 适用于 JDK 21 的 GraalVM
- 适用于 JDK 17 的 GraalVM
- 存档
- 开发版本
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
方法的实现。简单的 load
或 loadLibrary
调用就可以完成
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 指针的类型安全表示。JClass
、JMethodID
和 JObject
都是指针。由于上述定义,您现在有了 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
一样简单。