Java JNI 学习笔记

Java JNI 学习笔记

JNI(Java Native Interface)是 Java 提供的一种接口,使得 java 代码可以与其他语言(如 C 和 C++)编写的代码进行交互。具体来说,JNI 允许你在 Java 中调用本地(Native)代码,或者从本地代码调用 Java 方法。

基本概念

  • jni.h:这是 JNI 的头文件,使用 javac 生成,定义了 JNI 的接口函数和数据结构
  • jni.cpp:这是实现了本地方法的 C++ 文件,包含了具体的本地代码
  • jni.so:这是生成的共享库文件(在 windows 上为 .dll 文件),java 程序通过它调用本地方法

Demo:java 调用 C++ 代码

编写 java 类

创建一个 Java 类并声明一个本地方法。

// HelloJNI.java
public class HelloJNI {
    // 加载本地库
    static {
        System.loadLibrary("hello"); // Load native library at runtime
                                     // hello.dll (Windows) or libhello.so (Unixes)
    }
    
    // 声明一个本地方法
    private native void sayHello();

    // 主方法
    public static void main(String[] args) {
        new HelloJNI().sayHello(); // 调用本地方法
    }
}

上面代码的静态代码块在这个类被类加载器加载的时候调用了 System.loadLibrary 库来加载一个 native 库 “hello”,这个库实现了 sayHello 函数。接下来,我们使用 native 关键字将 sayHello() 方法声明为本地实例方法。注意,一个 native 方法不包含方法体,只有声明。上面代码中的 main 方法实例化了一个 HelloJJNI 类的实例,然后调用了本地方法 sayHello()

生成 C++ 头文件

编译 Java 类并使用 javac 生成 JNI 所需要的 C++ 头文件。

# 编译 Java 类并生成 C++ 头文件 
javac -h . HelloJNI.java

此时会生成一个名为 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: ()V
 */
JNIEXPORT void JNICALL Java_HelloJNI_sayHello(JNIEnv *, jobject);

# ifdef __cplusplus
}
# endif
# endif

上面的头文件生成了一个 Java_HelloJNI_sayHello 的 C 函数:

JNIEXPORT void JNICALL Java_HelloJNI_sayHello(JNIEnv *, jobject);

将 java 的 native 方法转换成 C 函数声明的规则是这样的:Java_{package_and_classname}_{function_name}(JNI arguments)。包名中的点换成单下划线。需要说明的是生成函数中的两个参数:

  1. JNIEnv *:这是一个指向 JNI 运行环境的指针,我们可以通过这个指针访问 JNI 函数
  2. jobject:指代 java 中的 this 对象

头文件中有一个 extern “C”,同时上面还有 C++ 的条件编译语句,这里的函数声明是要告诉 C++ 编译器:这个函数是 C 函数,请使用 C 函数的签名协议规则去编译!因为我们知道 C++ 的函数签名协议规则和 C 的是不一样的,因为 C++ 支持重写和重载等面向对象的函数语法。

编写 C++ 实现

创建一个 C++ 文件,实现头文件中声明的本地方法。

// HelloJNI.cpp
# include <jni.h>
# include <iostream>
# include "HelloJNI.h"

// 实现本地方法:注意函数名称需要和头文件中的函数名称保持一致
JNIEXPORT void JNICALL Java_HelloJNI_sayHello(JNIEnv *env, jobject obj) {
    std::cout << "Hello from C++!" << std::endl;
}

编译 C++ 代码生成共享库

将 C++ 文件编译为共享库文件:

# Linux/macOS
g++ -shared -fpic -o libhello.so -I${JAVA_HOME}/include -I${JAVA_HOME}/include/linux HelloJNI.cpp
g++ -shared -fpic -o libllm_jni.so -I${JAVA_HOME}/include -I${JAVA_HOME}/include/linux llm_jni.cpp
# Windows
g++ -shared -o hello.dll -I"%JAVA_HOME%\include" -I"%JAVA_HOME%\include\win32" HelloJNI.cpp

运行 Java 程序

确保共享库文件位于 Java 的库路径中,然后运行 java 程序:

# 运行 Java 程序
java -Djava.library.path=. -cp . HelloJNI

# -Djava.library.path=. : 指定 java 查找本地库的路径
# -cp . :设置 java 的类路径(class path),即查找 java 类文件的路径

执行上述命令后,你应该会看到输出:

Hello from C++!

在 java 和 Native 代码之间传递参数和返回值

传递基本类型

传递 java 的基本类型是非常简单而直接的,一个 jxxx 之类的类型已经定义在本地系统中了,比如:jint,jbyte,jshort,jlong,jfloat,jdouble,jchar 和 jboolean 分别对应 java 的 int,byte,short,long,float,double,char 和 boolean 基本类型。

Java JNI 程序:

public class TestJNIPrimitive {
   static {
      System.loadLibrary("myjni"); // myjni.dll (Windows) or libmyjni.so (Unixes)
   }

   // Declare a native method average() that receives two ints and return a double containing the average
   private native double average(int n1, int n2);

   // Test Driver
   public static void main(String args[]) {
      System.out.println("In Java, the average is " + new TestJNIPrimitive().average(3, 2));
   }
}

生成头文件:javac -h . TestJNIPrimitive.java

头文件 TestJNIPrimitive.h 中包含了一个函数声明:

JNIEXPORT jdouble JNICALL Java_TestJNIPrimitive_average(JNIEnv *, jobject, jint, jint);

可以看到,这里的 jint 和 jdouble 分别表示 java 中的 int 和 double。

TestJNIPrimitive.cpp 的实现如下:

// TestJNIPrimitive.cpp
# include <jni.h>
# include <iostream>
# include "HelloJNI.h"

JNIEXPORT jdouble JNICALL Java_TestJNIPrimitive_average(JNIEnv *env, jobject obj, jint n1, jint n2) {
    std::cout << "In C++, the numbers are " << n1 << " and " << n2  << std::endl;
    jdouble result;
    result = ((jdouble)n1 + n2) / 2.0;
    return result;
}

编译为共享库并运行:

g++ -shared -fpic -o libmyjni.so -I${JAVA_HOME}/include -I${JAVA_HOME}/include/linux TestJNIPrimitive.cpp
java -Djava.library.path=. -cp . TestJNIPrimitive

传递字符串

Java JNI 程序:

public class HelloJNI {
    static {
        System.loadLibrary("hello");
    }

    public native String sayHello(String msg);
    
    public static void main(String[] args) {
        String res = new HelloJNI().sayHello("Hello, JNI");
        System.out.println("JNI Results: " + res);
    }
}

生成头文件:javac -h . HelloJNI.java

JNIEXPORT jstring JNICALL Java_HelloJNI_sayHello(JNIEnv *, jobject, jstring);

编写 C++ 实现:

// HelloJNI.cpp
# include <jni.h>
# include <iostream>
# include "HelloJNI.h"

JNIEXPORT jstring JNICALL Java_HelloJNI_sayHello(JNIEnv *env, jobject obj, jstring inJNIStr) {
    if (inJNIStr == NULL) {
        return NULL;
    }
    
    // Convert the JNI String (jstring) into C-String (char*)
    const char* inCStr = env->GetStringUTFChars(inJNIStr, NULL);
    if (inCStr == NULL) {
        return NULL; 
    }
    // Log the received string
    std::cout << "In C++, the received string is: " << inCStr << std::endl;
    // Perform operations on the string
    std::string resultStr = std::string(inCStr) + " from C++";
    // Release the JNI String resources
    env->ReleaseStringUTFChars(inJNIStr, inCStr);
    // Convert the modified C-string back into JNI String (jstring) and return
    return env->NewStringUTF(resultStr.c_str());
}

注意,传递一个字符串比传递基本类型要复杂得多,因为 java 的 String 是一个对象,而 C 的 string 是一个 NULL 结尾的 char 数组。因此,我们需要将 java 的 String 对象转换成 C 的字符串表示形式:char *。

前面我们提到,JNI 环境指针 JNIEnv * 已经为我们定义了非常丰富的接口函数来处理数据的转换:

  1. 调用 const char* GetStringUTFChars(jstring, jboolean*) 来将 JNI 的 jstring 转换成 C 的 char *
  2. 调用 jstring NewStringUTF(char*) 来将 C 的 char * 转换成 JNI 的 jstring

编译生成共享库并运行:

g++ -shared -fpic -o libhello.so -I${JAVA_HOME}/include -I${JAVA_HOME}/include/linux HelloJNI.cpp
java -Djava.library.path=. -cp . HelloJNI

参考资料

  1. Java Native Interface (JNI) 从零开始详细教程
  2. 一篇文章教你完全掌握 jni 技术
posted @ 2024-07-15 23:28  Lockegogo  阅读(12)  评论(0编辑  收藏  举报