杰瑞科技汇

JNI如何用C调用Java方法?

  1. 编写 Java 代码:定义一个包含 native 方法的 Java 类。
  2. 生成 JNI 头文件:使用 javacjavah (或新工具 javac -h) 工具生成 C/C++ 头文件,该文件包含了 native 方法的函数签名。
  3. 编写 C/C++ 代码:实现头文件中定义的函数,并在这些函数中调用 Java 的代码。
  4. 编译生成动态链接库 (DLL/SO):将 C/C++ 源代码编译成 Java 虚拟机可以加载的库文件。
  5. 运行 Java 程序:在 Java 程序中加载这个动态库,并调用 native 方法。

准备工作

为了演示,我们假设你有一个标准的 Java 项目结构:

JNI如何用C调用Java方法?-图1
(图片来源网络,侵删)
jni-demo/
├── src/
│   └── com/
│       └── example/
│           └── JniDemo.java
└── jni/
    └── JniDemo.c

步骤 1: 编写 Java 代码

我们创建一个 Java 类,其中包含一个 native 方法,这个 native 方法本身不实现任何逻辑,它只是一个“桥梁”,由 C/C++ 代码来真正实现。

src/com/example/JniDemo.java

package com.example;
public class JniDemo {
    // 声明一个 native 方法,注意没有方法体
    public native void callJavaMethod();
    public static void main(String[] args) {
        // 1. 加载包含 native 方法的动态库
        // 注意:这里只需要写库名,不需要写 .dll 或 .so
        System.loadLibrary("JniDemo");
        // 2. 创建 Java 对象实例
        JniDemo demo = new JniDemo();
        // 3. 调用 native 方法
        System.out.println("Java: 准备调用 C/C++ 代码...");
        demo.callJavaMethod();
        System.out.println("Java: C/C++ 代码执行完毕。");
    }
}

关键点:

  • native 关键字告诉编译器,这个方法的实现不在 Java 中,而是在本地代码(C/C++)中。
  • System.loadLibrary("JniDemo") 会尝试加载名为 JniDemo.dll (Windows) 或 libJniDemo.so (Linux/macOS) 的动态库,JVM 会在库路径(如 java.library.path)中查找这个文件。

步骤 2: 生成 JNI 头文件

我们需要一个“桥梁”文件来告诉 C/C++ 编译器,这个 callJavaMethod 函数应该是什么样子,这个文件由 JDK 的工具自动生成。

JNI如何用C调用Java方法?-图2
(图片来源网络,侵删)

编译 Java 文件

进入 src 目录,编译 Java 文件:

cd src
javac com/example/JniDemo.java

这会生成 com/example/JniDemo.class 文件。

生成头文件

JNI如何用C调用Java方法?-图3
(图片来源网络,侵删)

有两种方式生成头文件:

方式一 (传统方式,使用 javah)

javah 在较新版本的 JDK 中已被弃用,但很多旧项目仍在使用。

# -jni 选项可以省略,因为它是默认值
# -classpath 指定 .class 文件所在的目录
javah -classpath . com.example.JniDemo

执行后,会在当前目录(src)下生成一个 com_example_JniDemo.h 文件。

方式二 (推荐方式,使用 javac -h)

这是现代 JDK 推荐的方式。

# -h 后面指定头文件输出的目录
# -d 指定 .class 文件输出的目录,这里我们直接输出到当前目录
javac -h . -d . com/example/JniDemo.java

同样会生成 com_example_JniDemo.h 文件。

生成的头文件 com_example_JniDemo.h 内容如下:

/* DO NOT EDIT THIS FILE - it is machine generated */
#include <jni.h>
/* Header for class com_example_JniDemo */
#ifndef _Included_com_example_JniDemo
#define _Included_com_example_JniDemo
#ifdef __cplusplus
extern "C" {
#endif
/*
 * Class:     com_example_JniDemo
 * Method:    callJavaMethod
 * Signature: ()V
 */
JNIEXPORT void JNICALL Java_com_example_JniDemo_callJavaMethod
  (JNIEnv *, jobject);
#ifdef __cplusplus
}
#endif
#endif

解读头文件:

  • #include <jni.h>: 包含了 JNI 的核心定义,所有 JNI 程序都必须包含它。
  • JNIEXPORTJNICALL: 这是 JNI 函数的修饰符,用于告诉编译器和链接器这个函数是 JNI 导出的。
  • Java_com_example_JniDemo_callJavaMethod: 这是 C/C++ 函数的命名规则:Java_ + 包名(用下划线 _ 替换) + 类名 + 方法名。
  • (JNIEnv *, jobject): 这是函数的参数。
    • JNIEnv *: 这是一个指向 JNI 环境的指针,通过这个指针,你可以在 C/C++ 代码中调用所有 JNI 函数(比如创建对象、调用方法、获取字段等)。
    • jobject: 这是 Java 对象 this 的引用,当 callJavaMethod 是一个实例方法时,这个参数就是调用该方法的对象实例,如果是 static native 方法,这个参数的类型会是 jclass,代表类的 Class 对象。
  • void: 返回值类型,与 Java 方法 void 对应。

步骤 3: 编写 C/C++ 代码

现在我们来实现 Java_com_example_JniDemo_callJavaMethod 函数,并在其中调用 Java 的代码。

jni/JniDemo.c

#include <stdio.h>
#include "com_example_JniDemo.h" // 包含生成的头文件
// 实现 Java_com_example_JniDemo_callJavaMethod 函数
JNIEXPORT void JNICALL Java_com_example_JniDemo_callJavaMethod
  (JNIEnv *env, jobject this_obj) {
  printf("C: 成功进入 C/C++ 代码!\n");
  // --- 核心部分:在 C/C++ 中调用 Java 代码 ---
  // 1. 获取 Java 的 System 类
  jclass system_class = (*env)->FindClass(env, "java/lang/System");
  // 2. 获取 out 对象 (java.lang.System.out)
  jfieldID out_field = (*env)->GetStaticFieldID(env, system_class, "out", "Ljava/io/PrintStream;");
  jobject out_obj = (*env)->GetStaticObjectField(env, system_class, out_field);
  // 3. 获取 println 方法 (java.io.PrintStream.println(String))
  jclass print_stream_class = (*env)->FindClass(env, "java/io/PrintStream");
  jmethodID println_method = (*env)->GetMethodID(env, print_stream_class, "println", "(Ljava/lang/String;)V");
  // 4. 创建一个 Java String 对象
  const char *c_str = "Hello from C/C++!";
  jstring j_str = (*env)->NewStringUTF(env, c_str);
  // 5. 调用 Java 的 System.out.println() 方法
  (*env)->CallVoidMethod(env, out_obj, println_method, j_str);
  // 6. 释放局部引用 (非常重要!)
  (*env)->DeleteLocalRef(env, j_str);
  (*env)->DeleteLocalRef(env, out_obj);
  (*env)->DeleteLocalRef(env, print_stream_class);
  (*env)->DeleteLocalRef(env, system_class);
  printf("C: Java 代码调用完毕,返回 C/C++ 代码,\n");
}

关键 JNI 函数解释:

函数 作用
(*env)->FindClass(env, "className") 根据类名(用 代替 )查找 jclass 对象。
(*env)->GetStaticFieldID(env, jclass, "fieldName", "fieldSignature") 获取静态字段的 ID,签名 Ljava/lang/String; 表示一个 String 对象。
(*env)->GetStaticObjectField(env, jclass, fieldID) 获取静态字段的值(一个 jobject)。
(*env)->FindClass(env, "className") 查找另一个类(如 PrintStream)。
(*env)->GetMethodID(env, jclass, "methodName", "methodSignature") 获取方法的 ID,签名 (Ljava/lang/String;)V 表示接收一个 String 参数,返回 void。
(*env)->NewStringUTF(env, "c_string") 将 C 风格的字符串转换为 Java 的 jstring 对象。
(*env)->CallVoidMethod(env, obj, methodID, ...) 调用一个实例方法。obj 是调用该方法的对象。
(*env)->DeleteLocalRef(env, jobj) 非常重要! JNI 中的局部引用(通过 JNI 函数创建的)不会自动被垃圾回收,必须在不再使用时手动删除,否则会导致内存泄漏。

步骤 4: 编译生成动态链接库

这一步与你的操作系统密切相关。

在 Windows 上 (使用 MinGW)

你需要安装 MinGW 或 MSYS2,并确保 gcc 在你的 PATH 中。

# 进入 jni 目录
cd ../jni
# 编译命令
# -I 指定 jni.h 的路径,通常在 JDK 的 include 目录下
# -shared 生成动态链接库 (.dll)
# -o 指定输出的文件名
# JniDemo.c 是你的源文件
# -lws2_32 是可能需要的库,具体看情况
gcc -I"C:/Program Files/Java/jdk-11.0.12/include" -I"C:/Program Files/Java/jdk-11.0.12/include/win32" -shared -o JniDemo.dll JniDemo.c

在 Linux/macOS 上

使用 GCC 编译器。

# 进入 jni 目录
cd ../jni
# 编译命令
# -I 指定 jni.h 的路径,通常在 JDK 的 include 目录下
# -shared 生成动态链接库 (.so)
# -o 指定输出的文件名
# JniDemo.c 是你的源文件
# JDK 路径可能需要根据你的系统调整
gcc -I/usr/lib/jvm/java-11-openjdk-amd64/include -I/usr/lib/jvm/java-11-openjdk-amd64/include/linux -shared -o libJniDemo.so JniDemo.c

编译成功后,你会在 jni 目录下得到 JniDemo.dll (Windows) 或 libJniDemo.so (Linux/macOS)。


步骤 5: 运行 Java 程序

运行你的 Java 程序,JVM 需要能找到你刚刚生成的动态库。

在 Windows 上

  1. JniDemo.dll 复制到 jni-demo 根目录。
  2. 打开命令行,在 jni-demo 目录下运行:
java -Djava.library.path=src com.example.JniDemo

或者,你也可以将 JniDemo.dll 放在 System32 目录下,然后直接运行:

java com.example.JniDemo

在 Linux/macOS 上

  1. libJniDemo.so 复制到 jni-demo 根目录。
  2. 打开终端,在 jni-demo 目录下运行:
java -Djava.library.path=src com.example.JniDemo

预期输出

无论在哪个系统,你都应该看到类似下面的输出:

Java: 准备调用 C/C++ 代码...
C: 成功进入 C/C++ 代码!
Hello from C/C++!
C: Java 代码调用完毕,返回 C/C++ 代码。
Java: C/C++ 代码执行完毕。

这个输出清晰地展示了 Java 代码如何调用 C/C++ 代码,而 C/C++ 代码又如何反过来调用 Java 的方法,完成了完整的 JNI 交互。

分享:
扫描分享到社交APP
上一篇
下一篇