杰瑞科技汇

Linux C如何调用Java方法?

下面我将分步详细解释整个过程,从环境准备到具体编码,再到编译和运行。

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

核心思想

整个过程可以概括为以下几个步骤:

  1. 编写 Java 代码:创建一个 Java 类,其中包含你想要从 C 代码调用的方法,这些方法需要使用 native 关键字声明,表示它们将由本地代码(C/C++)实现。
  2. 生成 C 头文件:使用 Java 自带的 javac 编译 Java 代码,然后用 javah(在 JDK 8 及之前)或 javac -h(在 JDK 9 及之后)工具,根据编译后的 .class 文件生成一个 C/C++ 头文件(.h),这个头文件包含了 C 函数的声明,这些函数的名称和签名是 JNI 规定的。
  3. 编写 C 代码:创建一个 C 源文件(.c),实现步骤 2 中生成的头文件里声明的函数,在这个 C 函数内部,你可以使用 JNI 提供的 API 来创建 Java 对象、调用 Java 方法、访问 Java 字段等。
  4. 编译链接:使用 GCC 或 Clang 等 C 编译器,将你的 C 源文件编译成一个动态共享库(.so 文件),在编译时,需要链接 JNI 的库。
  5. 运行:在 Linux 终端中运行你的 C 程序,C 程序会加载 Java 虚拟机(JVM),通过 JNI 调用 Java 代码,并处理返回结果。

详细步骤与示例

让我们通过一个完整的例子来走一遍流程。

第 1 步:准备环境

确保你的系统已经安装了 JDK,打开终端,检查版本:

java -version
javac -version

你需要 java (运行时) 和 javac (编译器) 两个工具。

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

第 2 步:编写 Java 代码

创建一个名为 JNITest.java 的文件,这个类将包含一个 native 方法。

JNITest.java

public class JNITest {
    // 这是一个本地方法,由 C 语言实现
    public native void sayHello();
    // 一个普通方法,供 C 代码调用
    public void printMessage(String message) {
        System.out.println("Java 收到消息: " + message);
    }
    // 一个静态方法,供 C 代码调用
    public static int add(int a, int b) {
        System.out.println("Java 执行加法: " + a + " + " + b);
        return a + b;
    }
    public static void main(String[] args) {
        System.out.println("Hello from Java!");
    }
}

第 3 步:编译 Java 代码并生成 C 头文件

  1. 编译 .java 文件

    javac JNITest.java

    这会生成 JNITest.class 文件。

    Linux C如何调用Java方法?-图3
    (图片来源网络,侵删)
  2. 生成 C 头文件

    • 对于 JDK 9 及以上版本(推荐):
      javac -h . JNITest.java

      这会在当前目录下生成一个 JNITest.h 头文件。

    • 对于 JDK 8 及以下版本
      javac JNITest.java
      javah JNITest

      这会生成一个 com_example_JNITest.h(包名会体现在路径中)或 JNITest.h 文件。

生成的 JNITest.h 文件内容(简化后)

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

注意函数名 Java_JNITest_sayHello,这就是 JNI 规定的命名规则:Java_ + 包名(如果有) + 类名 + 方法名。

第 4 步:编写 C 代码

创建一个 jni_impl.c 文件来实现 JNITest.h 中声明的函数。

jni_impl.c

#include <stdio.h>
#include <jni.h> // 必须包含 JNI 头文件
#include "JNITest.h" // 包含我们生成的头文件
// 实现 Java_JNITest_sayHello 函数
JNIEXPORT void JNICALL Java_JNITest_sayHello(JNIEnv *env, jobject obj) {
    // 1. 获取 JNITest 类的引用
    jclass clazz = (*env)->GetObjectClass(env, obj);
    // 2. 获取 printMessage 方法的 ID
    // 方法名: "printMessage"
    // 描述符: "(Ljava/lang/String;)V" 表示接收一个 String 参数,返回 void
    jmethodID mid_print = (*env)->GetMethodID(env, clazz, "printMessage", "(Ljava/lang/String;)V");
    // 3. 创建一个 Java 字符串对象
    jstring message = (*env)->NewStringUTF(env, "你好,来自 C 世界的问候!");
    // 4. 调用 Java 对象的 printMessage 方法
    (*env)->CallVoidMethod(env, obj, mid_print, message);
    // 5. 释放局部引用(防止内存泄漏)
    (*env)->DeleteLocalRef(env, message);
    (*env)->DeleteLocalRef(env, clazz);
    // --- 调用静态方法 add 的示例 ---
    printf("--- 调用静态方法 add ---\n");
    // 1. 获取 JNITest 类的引用
    jclass clazz_static = (*env)->FindClass(env, "JNITest");
    // 2. 获取静态方法 add 的 ID
    // 描述符: "(II)I" 表示接收两个 int 参数,返回一个 int
    jmethodID mid_add = (*env)->GetStaticMethodID(env, clazz_static, "add", "(II)I");
    // 3. 调用静态方法
    jint result = (*env)->CallStaticIntMethod(env, clazz_static, mid_add, 10, 20);
    printf("C 收到 Java add 方法的返回值: %d\n", result);
    // 4. 释放局部引用
    (*env)->DeleteLocalRef(env, clazz_static);
}

关键点解释

  • JNIEnv *env:这是 JNI 的核心,是一个指向 JNI 环境的指针,所有 JNI 函数的第一个参数都是它。
  • jobject obj:对于实例方法,这个参数代表调用该方法的 Java 对象本身(this),对于静态方法,它会是 NULL
  • (*env)->...:在 C 语言中,JNIEnv 是一个指针的指针,所以需要这样调用函数,在 C++ 中,JNIEnv 是一个类对象,可以直接调用 env->...
  • 方法描述符:非常重要,它定义了方法的参数和返回类型。
    • ()V:无参数,返回 void。
    • (I)I:一个 int 参数,返回 int。
    • (Ljava/lang/String;)V:一个 java.lang.String 参数,返回 void。
    • ([B)[B:一个 byte 数组参数,返回 byte 数组。
    • 可以使用 javap -s 命令查看类的描述符:
      javap -s JNITest

第 5 步:编译链接生成动态库

使用 GCC 编译 jni_impl.c,生成 libjnitest.so 文件。

gcc -shared -fpic -o libjnitest.so -I${JAVA_HOME}/include -I${JAVA_HOME}/include/linux jni_impl.c -L${JAVA_HOME}/jre/lib/amd64/server -ljvm

命令参数解释

  • -shared:生成一个共享库(.so 文件)。
  • -fpic:生成位置无关代码,这是共享库的要求。
  • -o libjnitest.so:指定输出的库文件名。注意:库文件名通常以 lib 开头
  • -I${JAVA_HOME}/include -I${JAVA_HOME}/include/linux:指定 JNI 头文件的搜索路径。${JAVA_HOME} 是你 JDK 的安装目录,/usr/lib/jvm/java-11-openjdk-amd64
  • -L${JAVA_HOME}/jre/lib/amd64/server:指定 JVM 库的搜索路径。
  • -ljvm:链接 JVM 库,这是 JNI 调用的基础。

注意${JAVA_HOME}/jre/lib/amd64/server 这个路径在不同 JDK 版本和架构下可能不同,请根据你的实际情况调整,你也可以用 find /usr/lib/jvm -name "libjvm.so" 来找到它的确切位置。

第 6 步:编写 C 程序入口并运行

我们需要一个 C 程序来启动 JVM 并加载我们刚刚创建的库。

main.c

#include <jni.h>
#include <stdio.h>
// JNI_OnLoad 函数会在库被加载时由 JVM 自动调用
// 我们可以在这里做一些初始化工作,但本例中不需要
int main(int argc, char **argv) {
    JavaVM *jvm; // JVM 实例
    JNIEnv *env;  // JNI 环境指针
    jclass cls;
    jmethodID mid;
    // 1. 初始化 JVM 参数
    JavaVMInitArgs vm_args;
    JavaVMOption options[1];
    // 设置 JVM 的类路径,确保能找到 JNITest.class
    // "." 表示当前目录
    options[0].optionString = "-Djava.class.path=.";
    vm_args.version = JNI_VERSION_1_8; // 使用你需要的 JNI 版本
    vm_args.options = options;
    vm_args.nOptions = 1;
    vm_args.ignoreUnrecognized = 0;
    // 2. 创建 JVM
    jint res = JNI_CreateJavaVM(&jvm, (void **)&env, &vm_args);
    if (res != JNI_OK) {
        fprintf(stderr, "Cannot create Java VM\n");
        return -1;
    }
    // 3. 获取 JNITest 类
    cls = (*env)->FindClass(env, "JNITest");
    if (cls == NULL) {
        fprintf(stderr, "Cannot find JNITest class\n");
        (*jvm)->DestroyJavaVM(jvm);
        return -1;
    }
    // 4. 获取 sayHello 方法的 ID
    mid = (*env)->GetMethodID(env, cls, "sayHello", "()V");
    if (mid == NULL) {
        fprintf(stderr, "Cannot get Method ID for sayHello\n");
        (*jvm)->DestroyJavaVM(jvm);
        return -1;
    }
    // 5. 创建 JNITest 对象实例
    jobject obj = (*env)->NewObject(env, cls, mid);
    if (obj == NULL) {
        fprintf(stderr, "Cannot create JNITest object\n");
        (*jvm)->DestroyJavaVM(jvm);
        return -1;
    }
    // 6. 调用 sayHello 方法
    printf("C 程序准备调用 Java 的 sayHello 方法...\n");
    (*env)->CallVoidMethod(env, obj, mid);
    printf("C 程序调用 Java 方法完毕,\n");
    // 7. 销毁 JVM
    (*jvm)->DestroyJavaVM(jvm);
    return 0;
}

编译并运行主程序

  1. 编译 main.c

    gcc -o my_c_app main.c -L. -ljnistest
    • -o my_c_app:输出的可执行文件名为 my_c_app
    • -L.:告诉编译器在当前目录()下查找库文件。
    • -ljnistest:链接名为 jnitest 的库,编译器会自动在前面加上 lib,并加上 .so 后缀,去寻找 libjnitest.so
  2. 运行 my_c_app

    ./my_c_app

    如果运行时出现 libjnitest.so: cannot open shared object file 错误,是因为系统找不到你刚刚生成的 .so 文件,你有两种方法解决:

    • 方法一(推荐):将 .so 文件复制到标准库路径,如 /usr/lib
      sudo cp libjnitest.so /usr/lib
    • 方法二(临时):设置 LD_LIBRARY_PATH 环境变量。
      export LD_LIBRARY_PATH=.:$LD_LIBRARY_PATH
      ./my_c_app

预期输出

C 程序准备调用 Java 的 sayHello 方法...
Hello from Java!
Java 收到消息: 你好,来自 C 世界的问候!
--- 调用静态方法 add ---
Java 执行加法: 10 + 20
C 收到 Java add 方法的返回值: 30
C 程序调用 Java 方法完毕。

总结与注意事项

  1. 性能:JNI 调用有性能开销,因为它涉及本地代码和 JVM 之间的上下文切换和数据转换,不要在性能敏感的循环中进行频繁的 JNI 调用。
  2. 内存管理:在 JNI 中创建的引用(如 jobject, jclass, jstring)分为局部引用和全局引用,局部引用只在当前线程的 JNI 方法调用期间有效,方法返回后会被自动释放,如果你想在多次 JNI 调用之间保持一个 Java 对象的引用,需要将其转换为全局引用(NewGlobalRef),并在用完后手动释放(DeleteGlobalRef),忘记释放会导致内存泄漏。
  3. 异常:JNI 调用可能会抛出 Java 异常,C 代码需要检查异常是否发生(ExceptionCheck),如果发生,必须处理(ExceptionDescribe 打印异常信息,ExceptionClear 清除异常),否则后续的 JNI 调用会失败。
  4. 线程:默认情况下,一个 JVM 只能被一个线程附加,如果你想在 C 程序的多个线程中使用 JNI,每个线程都需要通过 AttachCurrentThread 附加到 JVM,并在结束时用 DetachCurrentThread 分离。
  5. 复杂性:JNI 本身很复杂,特别是当处理复杂数据类型(如数组、对象)时,对于更复杂的交互,可以考虑使用 JNA (Java Native Access)Swig 等工具,它们可以简化本地代码的编写和绑定过程。
分享:
扫描分享到社交APP
上一篇
下一篇