当前位置: 首页 > 科技观察

Java筑基 - JNI到底是个啥

时间:2023-03-14 18:35:25 科技观察

Java基础-什么是JNI?对于一些比较底层的功能,本文我们将顺藤摸瓜,介绍一下jni及其使用。首先回顾一下jni的主要功能。从jdk1.1开始,jni标准就成为了java平台的一部分。它提供了一系列的API,可以让java与其他语言进行交互,实现java代码中调用其他语言的功能。通过jni的调用,可以实现这些功能:通常我们使用jni来调用c或c++中的代码。在上一篇文章中,我们使用了如下流程来描述native方法的调用流程:JavaCode->JNI->C/C++Code但是准确的说,这个流程并不严谨,因为最终执行的并不是原始的c/c++代码,而是编译链接的动态链接库。所以,我们把这个流程从简单的代码调用层面升级,把jni调用流程提升到jvm和操作系统层面,并且增加了一些细节来完善:看到这里,可能有朋友要问了,不是吗?也就是说java语言是跨平台的,这种与操作系统本地编译的动态链接库的交互,会不会让java失去跨平台的可移植性?针对这个问题,大家可以回忆一下之前安装jdk的经历,在官网的下载列表中,为每个操作系统提供了不同版本的jdk,比如windows、linux、macos版本等。在这些jdk中,不同的系统有不同的jvm实现。java语言的跨平台特性与其底层的jvm密不可分。正是不同版本的jvm在不同操作系统下的“翻译”工作,才能让编译后的字节码在不同平台下畅通无阻。跑步。在不同的操作系统下,c/c++或其他代码生成的动态链接库也会有所不同。比如在window平台下会编译成dll文件,在linux平台下会编译成so文件,在macos下会编译成jnilib文件。但是不同平台下的jvm会“约定俗成”的加载某种类型的动态链接库文件,这样依赖于操作系统的函数就可以正常调用了。这个过程可以参考下图来理解:当你对jni的整体调用过程有了一定的了解后,你也会好奇它在其他语言中是如何调用函数的过程中是如何实现的。下面我们就手写一个java程序来写一个调用c++代码的例子,来了解一下它的调用过程。1.准备java代码首先定义一个包含native方法的类如下,然后我们使用这个类中的native方法通过jni调用c++编写的动态链接库中的方法:publicclassJniTest{static{System.loadLibrary("MyNativeDll");}publicstaticnativevoidcallCppMethod();publicstaticvoidmain(String[]args){System.out.println("DLLpath:"+System.getProperty("java.library.path"));callCppMethod();}}在代码中主要完成以下任务:在静态代码块中,调用loadLibrary方法加载本地动态链接库,参数为动态链接库的文件名,不带扩展名。window平台下会加载dll文件,linux平台下会加载so文件,macos下会加载jnilib文件声明一个native方法。native关键字负责通知jvm,这里调用的方法是本地方法,在external的main方法中定义,打印加载dll文件的路径,调用本地方法2.生成头文件。使用c/c++实现native方法时,需要先创建.h头文件。简单地说,一个c/c++程序通常由一个头文件(.h)和一个定义文件(.c或.cpp)组成。头文件包含功能函数和数据接口的声明,定义文件用于编写程序的实现。在jdk8中,可以直接使用javac-h命令生成c/c++语言的头文件。如果你使用的是更早版本的jdk,需要执行javac编译class文件,然后执行javah-jni生成c/c++风格的头文件(jdk10新特性中删除了javah指令)。我们使用的jdk8简化了这一步,可以一步完成。在命令行窗口执行命令:javac-h./jniJniTest.java在指令中使用-h参数指定生成头文件的位置。最后一个参数是java源文件的名称。在这个过程中,完成了两个任务。首先生成class文件,其次在参数指定的目录下生成头文件。生成的头文件com_cn_jni_JniTest.h内容如下:/*DONOTEDITTHISFILE-itismachinegenerated*/#include/*Headerforclasscom_cn_jni_JniTest*/#ifndef_Included_com_cn_jni_JniTest#define_Included_com_cn_jni_JniTest#ifdef__cplusplusextern"C"{#endif/**Class:com_cn_jni_JniTest*Method:callCppMethod*Signature:()V*/JNIEXPORTvoidJNICALLJava_com_cn_jni_JniTest_callCppMethod(JNIEnv*,jclass);#ifdef__cplusplus}#endif#endifThegenerated头文件有点类似于大家熟悉的java接口,只是函数声明,没有具体实现。简单解释一下头文件中的代码:extern"C"告诉编译器这部分代码是使用C语言规则编译的。JNIEXPORT和JNICALL是jni中定义的两个宏。使用JNIEXPORT支持在外部程序代码中调用此代码。对于动态库中的方法,使用JNICALL定义调用函数时参数的入栈出栈约定可以调用大量封装在jni.h中的函数。第二个参数表示本机方法的调用者。当java代码中定义的native方法是静态方法时,这里的参数是jclass,非静态方法的参数是jobject接口。接下来我们创建一个cpp文件,引用头文件并实现里面的函数,也就是native方法真正会执行的逻辑:,jclass){printf("PrintFromCpp:\n");printf("Iamacppmethod!\n");}在方法的实现中添加一个简单的printf打印语句。方法实现完成后,我们需要将上面的cpp文件编译成动态链接库,提供给java中的native方法调用,所以下面的window环境需要安装gcc环境。3.window环境安装gcc环境。如果你不想为了生成dll而下载庞大的VisualStudio,MinGW是个不错的选择。简单的说就是一个windows版本下的gcc。那么估计又有同学会问了,什么是gcc?简单的说就是Linux系统下的C/C++编译器,通过它可以将源代码编译成可执行程序。首先从以下网址下载mingw-get-setup的安装程序:http://sourceforge.net/projects/mingw/#32位https://sourceforge.net/projects/mingw-w64/#64位需要注意,一定要根据系统位数安装对应的版本,否则后面生成的dll运行时可能会因为位数不匹配而报错。第一次实验时误安装了32位的MinGw,导致程序运行报如下错误:Exceptioninthread"main"java.lang.UnsatisfiedLinkError:F:\Workspace20\unsafe-test\src\main\java\com\cn\jni\jni\MyNativeDll.dll:Can'tloadIA32-bit.dllonaAMD64-bitplatform安装完成后,将MinGW\bin目录添加到系统环境变量PATH中,输入以下内容测试是否可以使用gcc的命令:gcc-v如果可以正常输出gcc的版本信息,说明gcc安装成功:在测试过程中,发现如果安装了64位的mingw,gcc安装完成后可以直接使用。但是如果你安装的是32位的mingw,还需要单独安装gcc,使用如下命令:mingw-getinstallgccgcc安装完成后,如果你想安装gdb或者make等调试或者编译的命令,也可以使用强大的mingw-get命令进行单机安装。4、生成动态链接库在gcc环境准备好的情况下,使用如下命令生成dll动态链接库:gcc-m64-Wl,--add-stdcall-alias-I"D:\ProgramFiles\java\jdk1.8.0_261\include"-I"D:\ProgramFiles\Java\jdk1.8.0_261\include\win32"-shared-oMyNativeDll.dllJniTestImpl.cpp简单解释一下各个参数的含义:-m64:编译cppcodeFor64-bitapplications-Wl,--add-stdcall-alias:-Wl表示将以下参数传递给链接器,参数--add-stdcall-alias表示标准调用后缀为@NN的符号将去掉后缀-I后导出:指定头文件的路径。生成头文件代码中引入的jni.h在这个目录下-shared:指定生成动态链接库。如果不使用这个flag,外部程序将无法连接-o:指定目标的名称,这里生成的动态链接库名为MyNativeDll.dllJniTestImpl.cpp:编译后的源程序文件名做了什么指令执行过程中,可以参考下图:执行过程中,以.cpp源代码和.h头文件为源文件,进行预处理、编译、汇编等操作第一的。图中省略了该阶段生成的一些中间文件,为编译后生成。.o二进制文件比较重要,就靠这个文件,最终生成动态链接库。执行完上面的命令后,会在当前目录下生成一个MyNativeDll.dll文件,然后运行之前准备的java代码:程序报错,因为没有找到我们的dll文件。解决方法有两种:直接将dll文件复制到默认加载目录下,具体路径可以通过System.getProperty("java.library.path")获取,该方法可能获取多个目录,放在任意一个目录下,可以在VMOption中修改启动参数,指定dll的存放目录:-Djava.library.path=F:\Workspace20\unsafe-test\src\main\java\com\cn\jni\jni再次执行,输出结果:DLLpath:F:\Workspace20\unsafe-test\src\main\java\com\cn\jni\jniPrintFromCpp:Iamacppmethod!可以看到程序加载dll的路径已经切换到了它的存放路径,并且通过jni调用成功,输出了c++中的代码逻辑。可以用下图来概括上面实现jni调用的过程:在对jni调用有了一个整体的了解之后,如果熟悉代理模式,也可以从代理模式的角度来理解jni,调用jni将进程中的各个角色带入代理模式:代理角色:jni类实现角色包括native方法:c/c++或其他语言实现的动态链接库Client:调用native方法的java类程序接口(抽象角色):接口在jni中的作用存在的比较弱,因为jni是跨语言的,所以不可能严格定义一个接口,同时应用到java和其他语言中。但是通过生成的.h头文件,一定程度上从接口规范上统一了java中的native方法和其他语言中的函数来描述代理模式的概览图:上图是基于标准代理模式做了一些修改,方便理解,因为这里的接口只是用于规范约束,所以客户端调用过程跳过接口,直接指向代理角色,然后代理角色调用实现功能角色的召唤。一般来说,jni起到代理或中介的作用。与普通代理不同的是,它只是在这里进行方法调用,并没有实现逻辑上的增强。通过这种模式,对java程序员隐藏了底层c/c++代码的实现细节,让我们可以专注于编写业务代码。总结本文基于之前对native方法的了解,介绍jni的相关知识。通过本文的学习,将帮助我们:理解java为什么可以跨平台,依赖于操作系统的底层操作是如何实现的理解native方法的调用过程,如果有必要,可以自己实现自己调用jni类接口复习一些C/C++知识当然使用jni也会带来一些弊端:在某个操作系统下使用jni标准时,编译本地代码生成动态链接库后,如果你想要将此程序移植到其他操作系统,需要在新平台上重新编译代码生成动态链接库。其他语言使用不当可能会导致程序错误,比如前面提到的使用C语言进行内存操作时可能由于没有及时回收内存而导致的内存泄漏。对于其他语言过度依赖Java会增加java与其他语言的耦合度,也会增加项目代码的维护成本。转载本文请联系码农公众号。