Java JNI(Java Native Interface)介绍

发布时间 2023-07-27 15:47:45作者: 与f

1. JNI 简介

众所周知,Java 的主要优势之一是它的可移植性,这意味着一旦我们编写并且编译了代码,这个过程的结果就是不依赖于平台的字节码。它可以像我们预期的那样运行在任何能够运行 Java 虚拟机的机器或设备上。

 

但是,有时我们确实需要使用一些为某些特定架构而进行本地编译的原生代码。例如:

  1. 需要对硬件执行某些操作
  2. 对性能要求非常苛刻
  3. 想要重用的现有库,而不是用 Java 重写它。

为了实现这一点,JDK 在我们的 JVM 中运行的字节码和原生代码(通常用 C 或 C++ 编写)之间搭建了一座桥梁。该桥梁就称为Java Native Interface。

 

2. JNI 如何工作

Java Code -> JNI -> C/C++ Code

Java 提供了 native 关键字,用于指明该方法的实现将由原生代码提供。native 关键字将我们的方法转换为一种抽象方法:

private native void aNativeMethod();

在这里,这个方法的实现不是由另一个 Java 类实现,而是在一个分离的原生动态共享库中实现。它将在内存中构造一个表,其中包含指向我们所有原生方法实现的指针,以便可以从 Java 代码中调用它们。

让 JNI 工作起来所需要的一些关键组件如下:

  • Java 代码 - 我们的类,它将至少包含一种本地方法。
  • 原生代码 - 我们原生代码的实际逻辑,通常使用 C 或者 C++ 代码。
  • JNI 头文件 - 这个 C/C++ 的头文件 (jni.h),包括了我们可以在原生程序中使用的所有JNI 元素。
  • C/C++ 编译器 - 用于为我们的平台生成原生共享库。

代码中的 JNI 组件包括了 Java 和 C/C++ 代码。

Java 代码:

  • "native" 关键字 - 标记为 native 的方法都必须在原生共享库中实现。
  • System.loadLibrary(String libname) - 一种静态方法,用于将共享库从文件系统加载到内存中,并使其包含的函数可用于我们的 Java 代码。

C/C++ 代码:

  • JNIEXPORT - 将共享库中的函数标记为可导出,它将包含在函数表中,因此 JNI 可以找到它。
  • JNICALL - 与 JNIEXPORT 结合使用,确保我们的方法可用于 JNI 框架。
  • JNIEnv - 一个包含方法的结构,可以使用我们的原生代码访问 Java 元素。
  • JavaVM - 一种让我们可以操纵正在运行的 JVM(甚至启动一个新的 JVM)的结构,向它添加线程、销毁它等等。

 

对于不同系统,C/C++库的调用

 

3. 编写 hello world JNI

 

3.1 创建 Java 类

受限编写我们的 Java 代码 HelloWorldJNI.java,具体如下,此类中用 native 关键字定义了需要 C/C++ 实现的原生方法 sayHello()

public class HelloWorldJNI {
    static {
        //这种方式需要把native.so放在jdk/bin目录下
        System.loadLibrary("native");
        //System.load("C:\\Users\\Administrator\\source\\repos\\ProjectNewDll\\x64\\Debug\\ProjectNewDll.dll");
    }
    
    public static void main(String[] args) {
        new HelloWorldJNI().sayHello();
    }
 
    // 定义原生sayHello()方法
    private native void sayHello();
}    

在代码中主要完成了以下工作:

  • 在静态代码块中,调用loadLibrary方法加载本地的动态链接库,参数为不包含扩展名的动态链接库库文件名。在window平台下会加载dll文件,在linux平台下会加载so文件,在mac os下会加载jnilib文件
  • 声明了一个native方法,native关键字负责通知jvm这里调用方法的是本地方法,该方法在外部被定义
  • main方法中,打印加载dll文件的路径,并调用本地方法

3.2 通过 Java 类生成 C/C++ 所需的头文件

通过 Java 类自动生成 sayHello() 方法的定义,并保存在 HelloWorldJNI.h 头文件中

javac -h . HelloWorldJNI.java

自动生成的 HelloWorldJNI.h 的头文件内容如下:

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

函数名是使用完全限定的包名、类名和方法名自动生成的。这个函数有两个参数,1 个是指向当前 JNIEnv 的指针,另一个是该方法附加到的 Java 对象或静态类名,HelloWorldJNI 类的实例。

简单的解释一下头文件中的代码:

  • extern "C"告诉编译器,这部分代码使用C语言规则来进行编译
  • JNIEXPORTJNICALLjni中定义的两个宏,使用JNIEXPORT支持在外部程序代码中调用该动态库中的方法,使用JNICALL定义函数调用时参数的入栈出栈约定
  • 函数名称由包名+类名+方法名组成,在该方法中有两个参数,通过第一个参数JNIEnv *的对象可以调用jni.h中封装好的大量函数 ,第二个参数代表着native方法的调用者,当java代码中定义的native方法是静态方法时这里的参数是jclass,非静态方法的参数是jobject

3.3 编写 C/C++ 文件,完成 sayHello() 的函数实现。

需要为 sayHello 函数的实现创建一个新的 c/cpp 文件,文件里面包含了函数的具体实现。将此文件和 .h 使用相同的命名。

以 C 代码举例,HelloWorldJNI.c 的具体实现如下:

#include <stdio.h>
#include "HelloWorldJNI.h"
 
JNIEXPORT void JNICALL Java_HelloWorldJNI_sayHello
  (JNIEnv* env, jobject thisObject) {
    printf("hello world!\n");
}

3.4 编译 C 代码,生成共享库

 我们已经实现了所有代码的编写,接下来需要从 C 代码编译成共享库,将共享库命名为 libnative.so 。

[root@wuhan hello]# export JAVA_HOME=/usr/lib/jvm/java-1.8.0-openjdk-1.8.0.312.b07-2.el8_5.x86_64/                     
[root@wuhan hello]# gcc -shared -fPIC -o libnative.so -I${JAVA_HOME}/include -I${JAVA_HOME}/include/linux HelloWorldJNI.c -lc

目前为止,我们当前的文件夹底下应该包含了如下文件:

[root@wuhan hello]# ls
HelloWorldJNI.c  HelloWorldJNI.class  HelloWorldJNI.h  HelloWorldJNI.java  libnative.so

 

windows下的编译:如

gcc -m64 -Wl,--add-stdcall-alias -I"D:\Program Files\Java\jdk1.8.0_261\include" 
-I"D:\Program Files\Java\jdk1.8.0_261\include\win32" 
-shared -o MyNativeDll.dll JniTestImpl.cpp

简单的解释一下各个参数的含义:

  • -m64 :将cpp代码编译为64位的应用程序
  • -Wl,--add-stdcall-alias-Wl表示将后面的参数传递给连接程序,参数--add-stdcall-alias表示带有标准调用后缀@NN的符号会被剥掉后缀后导出
  • -I:指定头文件的路径,在生成的头文件代码中引入的jni.h就在这个目录下
  • -shared:指定生成动态链接库,如果不使用这个标志那么外部程序将无法连接
  • -o:指定目标的名称,这里将生成的动态链接库命名为MyNativeDll.dll
  • JniTestImpl.cpp:被编译的源程序文件名

 

 

3.5 运行 Java 程序

最后,运行我们的 Java 程序,即可获得原生代码的输出 "hello world!"

[root@wuhan hello]# java -cp . -Djava.library.path=. HelloWorldJNI                                      
hello world!

或者,1.把库文件(dll/so)拷贝到加载目录;2.也可以在VM Option中修改启动参数,指定dll的存放目录

 

4. 定义带参数和返回值的函数方法

只调用原生的 C 代码并打印 "hello world!" 肯定是不能满足编码要求的。函数需要有非空的入参和返回值才能处理更多的事情。

在 HelloWorldJNI 类里面新增一个 sumIntegers 的方法,有两个 int 入参和一个 long 返回值。

public class HelloWorldJNI {
    static {
        System.loadLibrary("native");
    }
    
    public static void main(String[] args) {
        new HelloWorldJNI().sayHello();
        long sum = new HelloWorldJNI().sumIntegers(10, 20);
        System.out.println("sum:" + sum);
    }
 
    // 定义原生sayHello()方法
    private native void sayHello();
    // 定义原生sumIntegers方法,返回值是long,有两个int入参
    private native long sumIntegers(int first, int second);
}

查看生成的头文件

/* DO NOT EDIT THIS FILE - it is machine generated */
#include <jni.h>
/* Header for class HelloWorldJNI */
 
#ifndef _Included_HelloWorldJNI
#define _Included_HelloWorldJNI
#ifdef __cplusplus
extern "C" {
#endif
/*
 * Class:     HelloWorldJNI
 * Method:    sayHello
 * Signature: ()V
 */
JNIEXPORT void JNICALL Java_HelloWorldJNI_sayHello
  (JNIEnv *, jobject);
 
/*
 * Class:     HelloWorldJNI
 * Method:    sumIntegers
 * Signature: (II)J
 */
JNIEXPORT jlong JNICALL Java_HelloWorldJNI_sumIntegers
  (JNIEnv *, jobject, jint, jint);
 
#ifdef __cplusplus
}
#endif
#endif

C 代码新增 Java_HelloWorldJNI_sumIntegers 的实现:

#include <stdio.h>
#include "HelloWorldJNI.h"
 
JNIEXPORT void JNICALL Java_HelloWorldJNI_sayHello
  (JNIEnv* env, jobject thisObject) {
    printf("hello world!\n");
}
 
JNIEXPORT jlong JNICALL Java_HelloWorldJNI_sumIntegers
  (JNIEnv *env, jobject thisObject, jint first, jint second) {
    printf("received first:%d second:%d\n", first, second);
    long sum = (long)first + (long)second;
    return sum;
}

同样的编译运行步骤,可以得到 sumIntegers 的运行结果。

[root@wuhan hello]# java -cp . -Djava.library.path=. HelloWorldJNI
hello world!
received first:10 second:20
sum:30

可以在 Oracle 官方文档中查看 Java 类型和等价的 C JNI 类型。Java Native Interface Specification: 3 - JNI Types and Data Structures

 

5. 原生代码调用 Java 方法

Java 不仅能够调用原生代码,同样的,原生代码也可以调用 Java 方法。

首先先新建一个 UserData Java 类,这个类里定义的方法就是原生代码需要调用的。

public class UserData {    
    public String name;
    
    public String getUserName() {
        return name;
    }
}

再将 HelloWorldJNI 类改造一下,增加 createUser 和 printUserName 两个原生方法。在 main 里面调用 printUserName

public class HelloWorldJNI {
    static {
        System.loadLibrary("native");
    }
    
    public static void main(String[] args) {
        new HelloWorldJNI().sayHello();
        long sum = new HelloWorldJNI().sumIntegers(10, 20);
        System.out.println("sum:" + sum);
        HelloWorldJNI instance = new HelloWorldJNI();
        UserData newUser = instance.createUser("LeBron James");
        instance.printUserName(newUser);
    }
 
    // 定义原生sayHello()方法
    private native void sayHello();
    // 定义原生sumIntegers方法,返回值是long,有两个int入参
    private native long sumIntegers(int first, int second);
    // 定义原生createUser方法
    public native UserData createUser(String name);
    //定义原生printUserName方法
    public native String printUserName(UserData user);
}

在 HelloWorldJNI.c 中添加原生方法 createUser 和 printUserName 的实现。

#include <stdio.h>
#include "HelloWorldJNI.h"
 
JNIEXPORT void JNICALL Java_HelloWorldJNI_sayHello
  (JNIEnv* env, jobject thisObject) {
    printf("hello world!\n");
}
 
JNIEXPORT jlong JNICALL Java_HelloWorldJNI_sumIntegers
  (JNIEnv *env, jobject thisObject, jint first, jint second) {
    printf("received first:%d second:%d\n", first, second);
    long sum = (long)first + (long)second;
    return sum;
}
 
JNIEXPORT jobject JNICALL Java_HelloWorldJNI_createUser
  (JNIEnv *env, jobject thisObject, jstring myName){
    // 创建 UserData 类对象
    jclass userDataClass = (*env)->FindClass(env, "UserData");
    jobject newUserData = (*env)->AllocObject(env, userDataClass);
 
    // 获取需要UserData类的name成员字段
    jfieldID nameField = (*env)->GetFieldID(env, userDataClass , "name", "Ljava/lang/String;");
 
    // 给UserData类的name成员赋值
    (*env)->SetObjectField(env, newUserData, nameField, myName);
    
    // 返回创建的对象
    return newUserData;
}
 
JNIEXPORT jstring JNICALL Java_HelloWorldJNI_printUserName
  (JNIEnv *env, jobject thisObject, jobject userData){
    // 获取createUser创建的userData类
    jclass userDataClass = (*env)->GetObjectClass(env, userData);
    
    // 获取getUserName方法id
    jmethodID methodId = (*env)->GetMethodID(env, userDataClass, "getUserName", "()Ljava/lang/String;");
 
    // 调用getUserName方法,获取返回值,保存到result
    jstring result = (jstring)(*env)->CallObjectMethod(env, userData, methodId);
    
    // 打印getUserName获取的返回值
    printf("My name is: %s\n", (*env)->GetStringUTFChars(env, result, NULL));
    
    return result;
}

生成头文件,编译共享库,运行 Java 程序,可以得到如下结果:

[root@wuhan hello]# javac -h . HelloWorldJNI.java        
[root@wuhan hello]# gcc -shared -fPIC -o libnative.so -I${JAVA_HOME}/include -I${JAVA_HOME}/include/linux HelloWorldJNI.c -lc     
[root@wuhan hello]# java -cp . -Djava.library.path=. HelloWorldJNI         
hello world!
received first:10 second:20
sum:30
My name is: LeBron James

在获取类成员或调用类方法的时候,最后一个参数 "Ljava/lang/String;" 或 "()Ljava/lang/String;" 是参数或者方法的签名。签名的格式如图所示:

 JNI 的函数使用说明可参考 Java Native Interface Specification: 4 - JNI Functions

 

 

 

 

 

转:https://blog.csdn.net/yaojingqingcheng/article/details/123497697

 https://www.jb51.net/article/215627.htm