zoukankan      html  css  js  c++  java
  • JNI简易入门

    JNI简介

    JNI(Java Native Interface)是JDK的一部分,提供了若干API实现了Java和其他语言的通信(主要是C/C++)。JNI主要用于以下场景:

    • 贴近硬件底层的功能,Java无法实现;
    • 复用已有的程序(非Java开发);
    • 对部分代码有较高的性能要求,如矩阵运算、图形渲染等;

    在java的源码中,也多处使用了JNI,例如在Thread.java中,底层用于新建线程的方法,就是通过JNI,使用本地语言实现的。

    Java的特点的跨平台、可移植性强。但由于运行在JVM中,相对于本地语言来说,性能方面有一定的劣势。使用JNI可以使Java可以与本地语言互相通信(Java中可以调用本地语言的方法,本地语言也可以调用Java的方法),一定程度上提升代码的性能,但由于本地语言的引入,又让Java失去了平台移植的特性。

    使用JNI时,本地语言通常是C/C++,可以看成Java与C/C++的混合开发。需要了解C/C++基本法语言 、头文件、动态链接库、调用约定等概念。

       

    使用JNI的步骤如下:

    1. 编写.java文件,需要使用本地语言实现的方法,添加native关键字,并且不需要实现(不在.java文件中实现)。
    2. 使用javac工具,编译.java文件,生成.class文件。
    3. 使用javah工具,生成.h头文件。
    4. 新建与.h文件对应的.cpp文件,实现.h文件中的函数。
    5. 编译.cpp文件 ,生成动态链接库文件(Windows下为dll文件,Linux下位so文件)
    6. 联合.class文件,使用java工具运行。

    如下图所示:

    《图片来自百度百科》

       

    第一个JNI程序

    下面举一个例子,在Java中,使用C语言的printf()函数,输出"Hello World"。根据上面的流程,一步步执行。

    1、编写java文件

    新建一个HelloWorld.java文件,文件的内容如下:

    public class HelloWorld {

    public native void showHelloWorld();

       

    static {

    System.out.println(System.getProperty("user.dir"));

    System.out.println(System.getProperty("java.library.path"));

    System.loadLibrary("HelloWorld");

    }

       

    public static void main(String[] args) {

    new HelloWorld().showHelloWorld();

    }

    }

    有两点需要注意:

    • showHelloWorld()方法声明中有native关键字,表示该方法使用JNI,使用本地语言实现,在java文件中,不需要实现也能通过编译。
    • 在静态区块中,调用System.loadLibrary()方法,加载一个动态链接库。这个动态链接库等会由我们生成(在Windows下为dll文件,在Linux下为so文件 ),我们只需要指定文件名,JVM会根据当前的操作系统选择合适的文件格式。

    此外,静态区块中还输出了两个JVM的系统属性,后面会详细说明这两个属性。

       

    2、使用javac编译

    和编译普通的Java程序一样,编译前先新建一个out文件夹,使用-d参数编译。

    javac -d ./out HelloWorld.java

    如果没有错误,将会在out文件夹下,生成HelloWorld.class文件。

       

    3、使用javah生成头文件

    javah工具也是JDK的一部分,专门用于JNI开发,根据.java文件,生成.h文件。

    使用方法与javac类似:

    javah HelloWorld

    注意,不要带有java后缀名。正常的话,会在.java的同级文件夹,生成.h文件。

    本例中,生成的HelloWorld.h的内容如下:

    jni.h由JDK提供,是JNI的一部分,该文件存放在<JAVA_HOME>/include路径下。

    JNIEXPORT和JNICALL都是宏定义,不同平台的定义不同,在Windows下的定义如下:

    __declspec(dllexport)表示声明的函数为导出函数。

    __stdcall是调用约定的一种,表示函数参数从右至左入栈,调用接受后,函数内部完成堆栈清理。

    以上定义可以在jni_md.h文件中找到,该文件位于<JAVA_HOME>/include/win32

    注意jni.h与jni_md.h所在的路径,后面编译动态链接库时,会用到

       

    该头文件中声明了一个函数——Java_HelloWorld_showHelloWorld(),该函数就是HelloWorld.java文件中,使用native声明的showHelloWorld()方法。native方法在没有重载的情况下,与本地代码中的函数对应关系如下:

    Java_<包名>_<类名>_<方法名>()

       

    4、编写cpp文件,实现头文件中的函数

    在HelloWorld.h所在的目录下,新建HelloWorld.cpp文件,文件内容如下:

    #include "HelloWorld.h"

    #include <stdio.h>

       

    JNIEXPORT void JNICALL Java_HelloWorld_showHelloWorld

    (JNIEnv *, jobject)

    {

    printf("Hello World");

    }

    注意第一行,包含了javah生成的HelloWorld.h文件,并对头文件中的函数进行了实现。

       

    5、编译cpp文件位动态链接库

    不同平台的编译工具五花八门,例如在Window下,可以直接使用cl.exe编译;或编写Makefile,使用nmake编译;又或者编写vcxproj文件,使用msbuild编译。在Linux平台下,可以使用gcc编译,也可以编写Makefile,使用make工具编译。

    为了尽可能保证可移植性,这里选择CMake作为编译工具,让本地代码尽可能的支持多平台。CMake的使用说明可以参考《CMake简易入门》。

       

    在HelloWorld.cpp所在的目录下,新建CMakeLists.txt文件,文件内容如下:

    # 指定cmake的最低版本

    cmake_minimum_required(VERSION 2.8)

       

    # 指定项目名

    project(HelloWorld)

       

    # 添加头文件

    include_directories($ENV{JAVA_HOME}/include $ENV{JAVA_HOME}/include/win32)

       

    # 设置生成目录

    SET(LIBRARY_OUTPUT_PATH ${PROJECT_BINARY_DIR})

       

    # 生成链接库文件,三个参数分别是链接库名、链接库类型、源码文件

    add_library(${PROJECT_NAME} SHARED HelloWorld.cpp)

    第3个命令,指定头文件jni.h与jni_md.h所在的路径。

    第5个命令,表示要编译生成一个动态链接库文件,第一个参数指定动态链接库文件名。PROJECT_NAME是cmake内置的变量,其值有第2个命令配置为HelloWorld。本例中,需要和第1步中,System.loadLibrary()入参相同。

       

    编写结束后,打开VS的命令行窗口。VS的命令行窗口有x86、x64两种,需要和JDK位数对应。JDK的位数,可以使用命令"java -version"查看:

       

    打开VS的命令行后,cd切换到CMakeLists.txt所在的目录,输入以下命令,生成Makefile(本例子使用nmake工具编译):

    cmake -G "NMake Makefiles" -B .out .

    成功运行后,在out目录下,会生成以下文件:

    之后,使用以下命令(在CMakeLists.txt所在的目录),编译生成dll文件:

    cmake --build ./out --target HelloWorld

    或者可以使用cd命令进入out文件夹,之后使用namke命令编译:

    cd out

    nmake

    无论是那种方法,编译成功后,均会在out目录下,生成HelloWorld.dll文件(还有其他文件,这里不关心):

       

    6、使用java工具,运行测试

    命令行窗口切换到out目录,使用以下java工具,运行HelloWorld.class程序:

    java HelloWorld

    如果前面步骤没有问题,会有以下输出:

       

    先看末尾,红框中的"Hello World"就是用本地代码——printf()输出的结果,这说明Java与C/C++的交互成功了。

    再回到HelloWorld.java中,System.loadLibrary()仅根据一个文件名就找到了动态链接库文件,这里引出一个问题,动态链接库文件要保存在哪里?是不是和本例中,和.class文件保存在同级目录就可以了?

    答案是否定的。System.loadLibrary()加载动态连接库时,会到指定的路径中的搜索,这个路径保存在JVM系统参数java.library.path中。该参数实际上就是本地系统的环境变量PATH加上当前路径(注意该参数的末尾,";."表示当前路径,也就是JVM系统参数user.dir的值)。如果在这些路径中找不到指定的动态链接库,就会抛出以下java.lang.UnsatisfiedLinkError

       

    使用IDE

    但工程毕竟复制时,命令行编译就会变得毕竟繁琐。下面使用idea+VS的组合,完成JNI的开发。idea复制java部分的编译,VS负责C++的编译。(据说VS 2019支持Java ,那个时候估计只需要VS就可以了)

    同样按照上述的步骤。

    1. idea新建Java工程

      新建一个空的Java工程,命名为JNI_Demo。

      在src目录下,新建一个HelloWorld.java,文件内容和之前相同。

         

    2. 编译Java工程

      点击运行按钮,编译并允许,会有以下输出:

      抛出异常是正常的,我们还没有创建动态链接库文件,自然找不到。

      运行的目的是查看JVM系统参数user.dir的值,动态链接库生成后要保存在这个路径。

         

    3. 使用javah工具生成头文件

      右键工程视图中的HelloWorld.java文件,选择"Open in Terminal",在打开的命令行窗口使用javah工具,生成HelloWorld.h文件。

      成功后,可以在工程视图中看到HelloWorld.h文件。

         

    4. 使用VS新建动态链接库工程

      右键工程视图中的HelloWorld.h文件,选择"Show in Explorer",查看该文件的保存位置。

      使用VS,在该位置创建一个动态链接库工程,同样命名为HelloWorld:

         

      右键工程视图的"Header Files"文件夹,选择add-Existing Item,添加HelloWorld.h文件到工程中。

         

      编写工程中的HelloWorld.cpp文件,内容与之前的类似。

         

    5. 编译动态链接库

      编译前,需要对工程进行配置 。

      首先选择动态库生成的位数,与JDK要相同。

         

      工程视图中,右键HelloWorld工程,选择"Properties"(或者使用Alt + F7),打开工程属性配置窗口。

      先配置动态库文件生成的位置,目标位置就是第1步中,user.dir的值(也就是java工程的根目录)。

         

      之后添加头文件路径,一共需要添加3个路径:

      这3个路径分别是jni.h、jni_md.h、HelloWorld.h这3个头文件所在的目录。

         

      配置正确,HelloWorld.cpp中的下滑红线会全部消失。右键项目,选择"Build",开始编译。

      编译成功的话,在idea的工程视图,可以看到HelloWorld.dll文件:

         

    6. 运行测试

      回到idea,工程视图中右键src/HelloWorld文件夹,选择"Mark Directory as"-"Exclusion"。不然运行程序会有以下错误:

         

      配置结束后,就可以正常运行程序:

       

    除了idea + VS的组合外,还有其他多种组合,没有固定搭配,选择合适自己的工具即可。

       

    进阶

    结束了吗?不,到这里为止才刚刚开始。通过JNI我们可以与本地代码相互通信,互相通信包括:

    • Java调用本地代码
    • 本地代码调用Java方法
    • Java向本地代码传递参数
    • 本地代码项Java返回数据

    我们目前只完成了第一步,后面的可以参考官方文档:

    https://docs.oracle.com/javase/8/docs/technotes/guides/jni/spec/jniTOC.html

    中文翻译可以百度"JNI-API完全手册"。

  • 相关阅读:
    Atitit.Java exe bat  作为windows系统服务程序运行
    Atitit. Object-c语言 的新的特性  attilax总结
    Atitit. Object-c语言 的新的特性  attilax总结
    Atitit。Time base gc 垃圾 资源 收集的原理与设计
    Atitit。Time base gc 垃圾 资源 收集的原理与设计
    Atitit.go语言golang语言的新的特性  attilax总结
    Atitit.go语言golang语言的新的特性  attilax总结
    Atitit.pdf 预览 转换html attilax总结
    Atitit.pdf 预览 转换html attilax总结
    Atitit.office word  excel  ppt pdf 的web在线预览方案与html转换方案 attilax 总结
  • 原文地址:https://www.cnblogs.com/foundkey/p/10213473.html
Copyright © 2011-2022 走看看