Bringder JNI如何用C语言实现?

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

JNI 是一个强大的框架,它允许 Java 代码和其他语言(主要是 C 和 C++)进行交互,通过 JNI,Java 可以调用本地库(如 .dll.so 文件)中的函数,反之亦然,这对于性能优化、访问操作系统底层功能或使用现有 C/C++ 库非常有用。

bringder jni c语言
(图片来源网络,侵删)

这篇教程将分为以下几个部分:

  1. 核心概念:理解 JNI 的基本工作原理。
  2. 完整实例:从零开始,创建一个完整的 JNI "Hello World" 项目。
  3. 关键数据类型:介绍 JNI 如何处理 Java 和 C 之间的数据类型转换。
  4. 高级操作:如何操作 Java 对象、数组和异常处理。
  5. 构建与运行:讲解如何编译和运行你的 JNI 代码。

核心概念

在深入代码之前,需要理解几个关键概念:

  • Java 声明 (native 关键字):在 Java 中,使用 native 关键字声明一个方法,表示这个方法的实现不在 Java 中,而是在本地代码(C/C++)中。

    public class HelloWorld {
        // 声明一个 native 方法
        public native void sayHello();
    }
  • 生成 C 头文件 (javah):Java 编译器本身不生成 C 代码,你需要使用 JDK 提供的 javac 编译 Java 文件,然后使用 javah (在 JDK 8 及之前版本) 或 javac -h (在 JDK 9+ 版本) 工具,根据编译后的 .class 文件生成一个 C/C++ 头文件(.h),这个头文件包含了本地函数的 C 声明,函数名经过了一种特殊的“修饰”以包含类名等信息。

    bringder jni c语言
    (图片来源网络,侵删)
  • 实现 C 函数:你需要在 C 文件中实现这个函数,函数名必须与头文件中声明的完全一致,并且第一个参数总是 JNIEnv *,第二个参数是 jobject (对于实例方法) 或 jclass (对于静态方法)。

  • JNIEnv 指针:这是 JNI 的核心。JNIEnv 是一个指向 JNI 环境的指针,它提供了大量函数(如 FindClass, GetMethodID, CallObjectMethod 等)让你可以在 C 代码中操作 Java 对象、调用 Java 方法、访问字段等。

  • 加载本地库:在 Java 代码中,你需要使用 System.loadLibrary("库的名字") 来加载你的本地库,这会告诉 JVM 在系统路径(如 Windows 的 PATH 或 Linux 的 LD_LIBRARY_PATH)中寻找对应的 .dll.so 文件。


完整实例:从零开始

我们将创建一个 Java 类,它调用一个 C 函数来打印 "Hello from C!"。

bringder jni c语言
(图片来源网络,侵删)

步骤 1: 创建 Java 文件

创建一个名为 HelloWorld.java 的文件。

// HelloWorld.java
public class HelloWorld {
    // 声明一个 native 方法
    public native void sayHello();
    // 在静态代码块中加载本地库
    static {
        // "HelloWorldJNI" 是你将要生成的 .dll 或 .so 文件的名字(不含扩展名)
        System.loadLibrary("HelloWorldJNI");
    }
    public static void main(String[] args) {
        new HelloWorld().sayHello();
    }
}

步骤 2: 编译 Java 文件并生成头文件

编译 Java 文件:

javac HelloWorld.java

生成 C 头文件。推荐使用 JDK 9+ 的新方法

# -h . 表示将头文件生成在当前目录
# -cp . 指定 classpath
javac -h . -cp . HelloWorld.java

执行后,你会看到生成了一个名为 HelloWorld.h 的文件。不要手动修改这个文件!

步骤 3: 查看 C 头文件 (HelloWorld.h)

这个文件定义了 C 函数的签名。

/* DO NOT EDIT THIS FILE - it is machine generated */
#include <jni.h>
/* Header for class HelloWorld */
#ifndef _Included_HelloWorld
#define _Included_HelloWorld
#ifdef __cplusplus
extern "C" {
#endif
/*
 * Class:     HelloWorld
 * Method:    sayHello
 * Signature: ()V
 */
JNIEXPORT void JNICALL Java_HelloWorld_sayHello
  (JNIEnv *, jobject);
#ifdef __cplusplus
}
#endif
#endif
  • JNIEXPORTJNICALL 是 JNI 要求的宏,用于指定函数的调用规范。
  • Java_HelloWorld_sayHello 是 C 函数的完整名称,遵循 Java_包名_类名_方法名 的规则,因为我们的类在默认包下,所以没有包名。
  • JNIEnv *jobject 是标准的 JNI 函数签名。

步骤 4: 实现 C 代码

创建一个 HelloWorld.c 文件,并实现函数。

// HelloWorld.c
#include <stdio.h>
#include "HelloWorld.h" // 包含生成的头文件
// 实现 Java_HelloWorld_sayHello 函数
JNIEXPORT void JNICALL Java_HelloWorld_sayHello(JNIEnv *env, jobject obj) {
    printf("Hello from C!\n");
}

步骤 5: 编译 C 代码为本地库

这一步是平台相关的。

在 Windows (使用 MinGW/g++) 上: 你需要生成一个 .dll 文件。-I 参数指定了 jni.h 的位置(通常在 JDK 的 include 目录下)。

g -shared -IC:\path\to\jdk\include -IC:\path\to\jdk\include\win32 HelloWorld.c -o HelloWorldJNI.dll
  • g++ 是 C++ 编译器,但它也能很好地编译 C 代码。
  • -shared 告诉编译器生成一个共享库(DLL)。
  • -I 指定头文件搜索路径。
  • -o HelloWorldJNI.dll 指定输出的 DLL 文件名,必须与 Java 代码中的 System.loadLibrary("HelloWorldJNI") 对应。

在 Linux/macOS (使用 GCC) 上: 你需要生成一个 .so 文件。

gcc -shared -I/usr/lib/jvm/java-11-openjdk-amd64/include -I/usr/lib/jvm/java-11-openjdk-amd64/include/linux HelloWorld.c -o libHelloWorldJNI.so
  • -shared 生成共享库。
  • -I 指定 jni.hjni_md.h 的路径,这个路径根据你的 JDK 安装位置而变化。
  • -o libHelloWorldJNI.so 指定输出的 SO 文件名,在 Linux/macOS 上,库文件通常以 lib 开头。

步骤 6: 运行 Java 程序

确保你的本地库(.dll.so)在系统的库搜索路径中。

在 Windows 上:HelloWorldJNI.dll 复制到你的项目根目录(或者添加其所在目录到 PATH 环境变量),然后运行:

java HelloWorld

你应该会看到输出:

Hello from C!

在 Linux/macOS 上:libHelloWorldJNI.so 复制到项目根目录,或者设置 LD_LIBRARY_PATH

# 将当前目录添加到库搜索路径
export LD_LIBRARY_PATH=.:$LD_LIBRARY_PATH
# 运行 Java 程序
java HelloWorld

你应该会看到输出:

Hello from C!

关键数据类型

JNI 使用自己的类型前缀来与 C 类型区分,以避免冲突。

C 类型 JNI 类型 描述
char jchar Unicode 字符 (16-bit)
short jshort 16-bit 整数
int jint 32-bit 整数
long jlong 64-bit 整数
float jfloat 32-bit 浮点数
double jdouble 64-bit 浮点数
void void 无类型

对于 Java 的 boolean,JNI 使用 jboolean (通常是 unsigned char)。 对于 Java 的 byte,JNI 使用 jbyte (通常是 signed char)。


高级操作

1 传递基本数据类型

修改 HelloWorld.java

public class HelloWorld {
    // ...
    public native int add(int a, int b);
    // ...
}

重新生成头文件并实现 C 代码:

// 在 HelloWorld.h 中会生成:
// JNIEXPORT jint JNICALL Java_HelloWorld_add(JNIEnv *, jobject, jint, jint);
JNIEXPORT jint JNICALL Java_HelloWorld_add(JNIEnv *env, jobject obj, jint a, jint b) {
    return a + b;
}

2 操作 Java 字符串

Java 字符串是 java.lang.String 对象,在 C 中通过 jstring 类型表示,你不能直接使用 C 的 printf 打印它,需要通过 JNIEnv 的函数进行转换。

public class HelloWorld {
    // ...
    public native void printString(String str);
    // ...
}

C 实现:

#include <stdio.h>
#include "HelloWorld.h"
JNIEXPORT void JNICALL Java_HelloWorld_printString(JNIEnv *env, jobject obj, jstring jstr) {
    // 1. 将 jstring 转换为 C 风格的 char*
    const char *cstr = (*env)->GetStringUTFChars(env, jstr, NULL);
    if (cstr == NULL) {
        return; // 内存分配失败
    }
    printf("The string from Java is: %s\n", cstr);
    // 3. 释放资源,非常重要!
    (*env)->ReleaseStringUTFChars(env, jstr, cstr);
}
  • GetStringUTFChars: 将 Java 字符串转换为 UTF-8 编码的 C 字符串。必须在使用完毕后调用 ReleaseStringUTFChars 来释放内存,否则会导致内存泄漏。

3 创建和操作 Java 对象

public class HelloWorld {
    // ...
    public native Person createPerson(String name, int age);
    // ...
}
// 假设有一个 Person 类
class Person {
    private String name;
    private int age;
    // ... constructor, getters, toString() ...
}

C 实现:

#include <stdio.h>
#include "HelloWorld.h"
JNIEXPORT jobject JNICALL Java_HelloWorld_createPerson(JNIEnv *env, jobject obj, jstring name, jint age) {
    // 1. 找到 Person 类
    jclass personClass = (*env)->FindClass(env, "Person");
    if (personClass == NULL) {
        // 处理异常
        return NULL;
    }
    // 2. 找到构造方法 (构造方法在 JNI 中名为 "<init>")
    jmethodID constructor = (*env)->GetMethodID(env, personClass, "<init>", "(Ljava/lang/String;I)V");
    if (constructor == NULL) {
        return NULL;
    }
    // 3. 调用构造方法创建对象
    jobject newPerson = (*env)->NewObject(env, personClass, constructor, name, age);
    return newPerson;
}
  • FindClass: 根据 类的全限定名 查找 jclass
  • GetMethodID: 查找方法(包括构造方法),签名 (Ljava/lang/String;I)V 表示:一个 String 参数 (Ljava/lang/String;),一个 int 参数 (I),返回类型 void (V)。
  • NewObject: 调用构造方法创建新对象。

构建与运行的最佳实践

手动编译命令很繁琐,尤其是在跨平台时,推荐使用构建工具如 CMake

CMake 示例 (CMakeLists.txt)

# 指定 CMake 最低版本
cmake_minimum_required(VERSION 3.10)
# 项目名称
project(HelloWorldJNI)
# 查找 JDK
find_package(JNI REQUIRED)
# 添加可执行文件(如果需要)
# add_executable(my_main Main.c)
# 添加共享库 (JNI 库)
add_library(HelloWorldJNI SHARED HelloWorld.c)
# 包含 JNI 头文件目录
target_include_directories(HelloWorldJNI PRIVATE ${JNI_INCLUDE_DIRS})
# 在 Windows 上,链接 jvm.lib
if(WIN32)
    target_link_libraries(HelloWorld PRIVATE ${JNI_LIBRARIES})
endif()

使用 CMake 的步骤:

  1. 创建一个 build 目录。
  2. build 目录中运行 cmake ..
  3. 然后运行 cmake --build .

CMake 会自动检测操作系统并生成正确的构建文件(如 Makefile 或 Visual Studio 项目),大大简化了编译过程。

总结与注意事项

  • 性能:JNI 调用有性能开销,因为它涉及到 Java 和 C 运行时之间的切换,避免在频繁调用的循环中使用 JNI。
  • 内存管理:从 C 中创建或获取的 JNI 对象(如 jstring, jobjectArray)需要通过 JNIEnv 的函数(如 NewGlobalRef, DeleteLocalRef)来正确管理引用,否则会导致内存泄漏。
  • 异常处理:JNI 函数调用失败(如 FindClass 找不到类),它不会像 Java 那样抛出异常,而是会设置一个异常标志,你必须通过 (*env)->ExceptionCheck(env) 来检查并处理,否则后续的 JNI 调用可能会失败。
  • 线程JNIEnv 是线程局部的,一个线程不能使用另一个线程的 JNIEnv,如果你想在多个线程中使用 JNI,每个线程都必须通过 AttachCurrentThread 附加到 JVM 并获取自己的 JNIEnv

JNI 是一个功能强大但复杂的工具,希望这份指南能帮助你入门!

-- 展开阅读全文 --
头像
dede list与artlist用法区别是什么?
« 上一篇 2025-12-18
织梦DedeCms arctype字段如何自定义与调用?
下一篇 » 2025-12-18

相关文章

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

目录[+]