C语言如何直接调用Java方法?

99ANYc3cd6
预计阅读时长 22 分钟
位置: 首页 C语言 正文

在C语言中直接“调用”Java方法是不可能的,因为C和Java是两种完全不同的语言,运行在不同的环境(C在本地操作系统,Java在Java虚拟机JVM)中,它们之间需要一个“桥梁”来进行通信。

c语言调用java中的方法
(图片来源网络,侵删)

这个桥梁就是 Java Native Interface (JNI)

JNI是Java平台的一部分,它定义了一套规范,允许Java代码与其他语言(主要是C和C++)编写的代码进行交互,通过JNI,你可以:

  1. 从Java代码调用C/C++本地方法(更常见)。
  2. 从C/C++代码调用Java方法(你当前的问题)。

下面我将详细解释如何实现 从C语言调用Java方法


核心思想

整个过程分为两个主要部分:

c语言调用java中的方法
(图片来源网络,侵删)
  1. Java端:创建一个类,声明一个或多个 native 方法,使用 javac 编译这个类,再用 javah (旧版) 或 javac -h (新版) 生成一个C语言头文件(.h),这个头文件包含了C函数需要遵循的签名。
  2. C端:实现头文件中定义的C函数,在这个C函数内部,你会使用JNI提供的API来:
    • 找到并加载目标Java类。
    • 获取要调用的Java方法的ID。
    • 创建Java对象(如果需要)。
    • 调用该方法,并传递参数。
    • 处理返回值。

详细步骤与示例

假设我们的目标是从C程序中调用Java类的 String sayHello(String name) 方法。

第1步:编写Java代码

我们创建一个Java类,其中包含一个 native 方法。

HelloJNI.java

public class HelloJNI {
    // 声明一个native方法,这个方法将由C语言实现
    public native String sayHello(String name);
    // 加载包含本地库的动态链接库 (.dll on Windows, .so on Linux)
    static {
        System.loadLibrary("hellojni"); // 注意:这里只写库名,不带lib前缀和.so/.dll后缀
    }
    public static void main(String[] args) {
        // 创建HelloJNI类的实例
        HelloJNI helloJNI = new HelloJNI();
        // 调用本地方法
        String result = helloJNI.sayHello("from C");
        System.out.println("Java received from C: " + result);
    }
}

第2步:编译Java代码并生成JNI头文件

  1. 编译Java类

    c语言调用java中的方法
    (图片来源网络,侵删)
    javac HelloJNI.java

    这会生成 HelloJNI.class 文件。

  2. 生成JNI头文件: 使用 javac-h 选项来生成C头文件,这个头文件定义了C函数的签名,C代码必须严格按照这个签名来实现。

    javac -h . HelloJNI.java

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

HelloJNI.h (自动生成)

/* DO NOT EDIT THIS FILE - it is machine generated */
#include <jni.h>
/* Header for class HelloJNI */
#ifndef _Included_HelloJNI
#define _Included_HelloJNI
#ifdef __cplusplus
extern "C" {
#endif
/*
 * Class:     HelloJNI
 * Method:    sayHello
 * Signature: (Ljava/lang/String;)Ljava/lang/String;
 */
JNIEXPORT jstring JNICALL Java_HelloJNI_sayHello
  (JNIEnv *, jobject, jstring);
#ifdef __cplusplus
}
#endif
#endif

关键点解读 HelloJNI.h:

  • #include <jni.h>: 包含了所有JNI定义的核心头文件。
  • JNIEXPORT, JNICALL: JNI特定的宏,用于指定函数的调用约定。
  • jstring: JNI中的字符串类型。
  • JNIEnv *: 指向JNI环境的指针,是JNI函数的“总开关”,几乎所有JNI操作都通过它来完成。
  • jobject: 指向Java对象的引用,它指向 HelloJNI 类的实例。
  • Java_HelloJNI_sayHello: C函数的命名规则是固定的:Java_ + 包名(如果有) + _ + 类名 + _ + 方法名。
  • (JNIEnv *, jobject, jstring): 函数参数列表。
  • Ljava/lang/String;: 这是JNI的“类型签名”,表示 java.lang.String 类型。(Ljava/lang/String;)Ljava/lang/String; 表示 sayHello(String): String

第3步:编写C/C++实现代码

我们来实现 HelloJNI.h 中声明的函数,这个函数将完成从C调用Java sayHello 的核心逻辑。

hellojni.c

#include <stdio.h>
#include "HelloJNI.h" // 包含我们生成的头文件
// JNIEXPORT jstring JNICALL Java_HelloJNI_sayHello(JNIEnv *, jobject, jstring);
JNIEXPORT jstring JNICALL Java_HelloJNI_sayHello(JNIEnv *env, jobject thisObj, jstring name) {
    // 1. 将Java String (jstring) 转换为C字符串 (const char*)
    const char *c_name = (*env)->GetStringUTFChars(env, name, NULL);
    if (c_name == NULL) {
        return NULL; // Out of memory
    }
    printf("C: Received name from Java: %s\n", c_name);
    // 2. 在C代码中进行一些处理
    char c_message[100];
    sprintf(c_message, "Hello, %s! I'm C.", c_name);
    // 3. 释放C字符串的内存,避免内存泄漏
    (*env)->ReleaseStringUTFChars(env, name, c_name);
    // 4. 创建一个新的Java String对象,用于返回
    jstring j_message = (*env)->NewStringUTF(env, c_message);
    return j_message;
}

代码解释:

  • JNIEnv *env: 这是JNI环境指针,我们用它来调用JNI函数。
  • (*env)->...: 在C语言中,JNIEnv 是一个指向函数指针结构的指针,所以需要这样解引用来调用其内部的函数。
  • GetStringUTFChars: 将JNI的 jstring 转换为C语言可读的UTF-8格式字符串。
  • ReleaseStringUTFChars: 非常重要! 必须释放 GetStringUTFChars 分配的资源,否则会导致内存泄漏。
  • NewStringUTF: 创建一个新的 jstring 对象,从C字符串转换而来,这个对象可以被返回给Java。

第4步:编译C代码为共享库

我们将C代码编译成动态链接库(共享库),Java程序可以通过 System.loadLibrary 来加载它。

在Linux/macOS上:

# -I 指定jni.h的路径,通常是JAVA_HOME/include
# -shared 生成共享库
# -o 指定输出的库名,必须与Java代码中 System.loadLibrary("hellojni") 的名字一致
gcc -I${JAVA_HOME}/include -I${JAVA_HOME}/include/linux -shared -o libhellojni.so hellojni.c

注意:库名必须是 lib<name>.so 的格式。

在Windows上 (使用MinGW或Visual Studio):

# -I 指定jni.h的路径
# -shared 生成DLL (动态链接库)
# -o 指定输出的库名,必须是 <name>.dll 的格式
gcc -I"C:\Program Files\Java\jdk-11.0.12\include" -I"C:\Program Files\Java\jdk-11.0.12\include\win32" -shared -o hellojni.dll hellojni.c

注意:库名必须是 <name>.dll 的格式,与 System.loadLibrary("hellojni") 中的名字一致,不需要 lib 前缀。

第5步:运行Java程序

确保编译好的共享库(.so.dll)位于Java能够找到它的地方,通常有两种方式:

  1. 放在Java库路径下:将 libhellojni.so (Linux) 或 hellojni.dll (Windows) 放在Java的 lib 目录下或系统PATH环境变量中包含的目录。
  2. 通过 -Djava.library.path 指定:这是最推荐的方式。
# 在Linux/macOS上
java -Djava.library.path=. HelloJNI
# 在Windows上
java -Djava.library.path=. HelloJNI

预期输出:

C: Received name from Java: from C
Java received from C: Hello, from C! I'm C.

关键JNI API总结

操作 JNI函数 说明
获取类 FindClass(env, "java/lang/String") 通过类全名获取 jclass 对象。
获取方法ID GetMethodID(env, clazz, "methodName", "(Ljava/lang/String;)V") 获取实例方法ID。GetStaticMethodID 用于静态方法。
调用方法 CallVoidMethod(env, obj, methodID, ...) 调用返回值为void的方法,还有 CallIntMethod, CallObjectMethod 等。
创建对象 NewObject(env, clazz, constructorID, ...) 创建Java对象实例。
基本类型转换 GetIntField, SetIntField 直接操作Java对象的成员变量。
字符串操作 NewStringUTF, GetStringUTFChars, ReleaseStringUTFChars 创建和转换Java字符串。
异常处理 ExceptionCheck(env), ExceptionDescribe(env) 检查并打印JNI调用过程中发生的Java异常。

重要注意事项

  1. 性能开销:JNI调用比普通Java方法调用慢得多,频繁地在Java和C之间切换会严重影响性能,应尽量减少JNI调用的次数,在C中完成批量计算。
  2. 内存管理:C语言没有自动垃圾回收,当你通过JNI分配内存(如 GetStringUTFChars)或创建对象时,必须手动释放,否则会导致内存泄漏。
  3. 线程安全JNIEnv线程绑定的,一个线程不能使用另一个线程的 JNIEnv,如果你的C代码需要被多个Java线程调用,必须确保每个Java线程调用C函数时,C函数内部使用的是属于该线程自己的 JNIEnv
  4. 类型签名:JNI的类型签名(如 I 代表 int, Ljava/lang/String; 代表 String)是固定的,但容易写错,可以使用 javap 工具来查看:
    javap -s HelloJNI
  5. 异常处理:在C代码中调用JNI函数后,如果Java端抛出了异常,JNI函数会返回一个错误值或NULL,但异常状态会保留,在继续执行其他JNI操作前,必须先处理(通常是清除)这个异常,否则C代码会处于不稳定状态。
    if ((*env)->ExceptionCheck(env)) {
        (*env)->ExceptionDescribe(env); // 打印异常信息
        (*env)->ExceptionClear(env);   // 清除异常
        // ... 错误处理逻辑
    }

通过以上步骤,你就可以成功地在C语言中调用Java的方法了,这为跨语言混合开发提供了强大的能力。

-- 展开阅读全文 --
头像
C语言中float类型如何正确使用?
« 上一篇 03-03
织梦批量替换文章描述怎么操作?
下一篇 » 03-03

相关文章

取消
微信二维码
支付宝二维码

目录[+]