- 适用于 JDK 24 的 GraalVM(最新)
- 适用于 JDK 25 的 GraalVM(早期访问)
- 适用于 JDK 21 的 GraalVM
- 适用于 JDK 17 的 GraalVM
- 存档
- 开发构建
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
方法的实现已加载。简单的 load
或 loadLibrary
调用即可。
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 指针的类型安全表示。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
一样简单。