首页 > 编程语言 >Java JNI(Java Native Interface)介绍

Java JNI(Java Native Interface)介绍

时间:2023-07-27 15:46:43浏览次数:44  
标签:原生 Java 代码 HelloWorldJNI env Interface JNI native

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

 

标签:原生,Java,代码,HelloWorldJNI,env,Interface,JNI,native
From: https://www.cnblogs.com/fps2tao/p/17584441.html

相关文章

  • [Javascript] removeEventListener
    Mistake1:Notusingthesamefunctionreference//Wrongbutton.addEventListener('click',()=>{console.log('click')})button.removeEventListener('click',()=>{console.log('click')})//Won'tremovet......
  • Java面试题 P9:hashCode与equals区别
    equals:1、用于定义对比两个对象的对比规则,来判断这两个对象什么时候是相等的,什么时候是不相等的2、默认使用object的equals,实际上就是==号,对比的是对象在栈中的引用的地址,如果是基本类型变量的话对比的是栈中的值,对比的是引用地址。hashCode:1、 ......
  • 【小实验】javascript 能够表述的最大整数
    作者:张富春(ahfuzhang),转载时请注明作者和引用链接,谢谢!cnblogs博客zhihuGithub公众号:一本正经的瞎扯打开浏览器的控制台,开始输入数值:输入:(16位十进制值)9999999999999998返回同样结果输入:9999999999999999返回:10000000000000000数值不一样,说明精度已经开始丢......
  • 直播平台软件开发,JavaWeb如何设置定时任务
    直播平台软件开发,JavaWeb如何设置定时任务1.在xml文件中添加监听器 <?xmlversion="1.0"encoding="UTF-8"?><web-appversion="2.4" xmlns="http://java.sun.com/xml/ns/j2ee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"......
  • java 读取scd文件
    Java读取scd文件的实现简介在Java中,通过使用合适的库和代码,可以很方便地读取和处理scd文件。scd文件是一种用于存储数据的结构化文件格式,常用于数据库备份和数据交换。在本文中,我们将介绍如何使用Java读取scd文件。我们将按照以下步骤进行操作:打开scd文件读取文件内容处理......
  • java 集成elt
    Java集成ELT概述ELT(提取、加载和转换)是一种常见的数据集成方法,用于将数据从源系统提取出来,加载到目标系统,并在加载过程中对数据进行转换和清洗。在Java应用程序中,集成ELT功能可以通过使用相关的Java库和框架来实现。本文将介绍如何在Java应用程序中集成ELT功能,并提供代码示例来......
  • java 获取随机名字
    Java获取随机名字的实现方法引言在Java开发过程中,有时候我们需要获取随机的名字,比如用于生成随机用户名、测试数据等。本文将介绍如何实现获取随机名字的功能,并给出具体的代码示例。实现步骤下面是获取随机名字的实现步骤,通过表格形式展示:步骤描述1.创建一个包含常......
  • java 读取pptx
    Java读取PPTX引言在现如今的信息时代,PPTX已经成为了一种常见的演示文稿格式。许多人在工作和学习中都需要读取和操作PPTX文件。而Java作为一种广泛使用的编程语言,也提供了许多库和工具来处理PPTX文件。本文将介绍如何使用Java来读取PPTX文件,并提供相应的代码示例。PPTX文件格式......
  • java 读取gbk文件
    Java读取GBK文件的方法在Java中,有时候我们需要读取和处理GBK编码的文件。尽管现代的编程环境一般都默认使用UTF-8编码,但仍然有一些旧的系统或遗留的文件使用GBK编码。本文将介绍如何在Java中读取GBK编码的文件,并提供相应的代码示例。了解GBK编码GBK是中文编码的一种常见方式,它支......
  • java 获取时间字符传
    Java获取时间字符串在Java中,我们经常需要获取并处理时间相关的信息。获取当前时间的字符串表示是一种常见的需求,可以用于日志记录、文件命名、时间戳等场景。本文将介绍如何使用Java获取时间的字符串表示,以及一些常见的时间格式化方式。获取当前时间的字符串表示在Java中,可以使......