实践JNI-Java调用C语言DLL库
前言
Java虽然是一个跨平台的语言,但是实践中很难避免需要使用其他语言,如Rust和C,来编写更加高性能的算法函数来提高性能,或者获取更好的管理内存的能力.
这就必须谈到JNI. JNI是自JDK1.1起推出的一种调用外部库的方法(还有一种方法是JNA),使用JNI可以使Java程序调用外部的C动态运行库来执行某些操作. 调用JNI是一个稍显繁琐和复杂的过程,但其实做一些简单实践并不难. 同时,因为Java和C语言的函数传参方式,数据类型(尤其是类)等等并不完全相同,使用JNI时需要尤其弄清逻辑.
在继续之前,我应该假设你具备以下知识点和技能:
- java 了解Java编译相关
- C和gcc编译器 了解动态库和编译相关命令
- 知道MinGW
环境:
- JDK 8
- Mingw64 13
简单实践
Java部分
写一个Main文件和A文件,其中Main.java包括主函数,创建了一个A实例,并调用A类的add方法.
1 | |
接下来的A.java文件就不太一样了,先上代码:
1 | |
首先,使用静态块来保证System.loadLibrary一定会加载A.dll这个文件(在Windows下为A.dll,Linux下为libA.so),然后,使用native修饰add这个方法,表明这是一个外部库实现的方法.
需要指出System.loadLibrary寻找文件的机制:
如果填入为xx,JVM会在JDK自带的lib库中寻找,再到-Djava.library.path参数指定的目录寻找
如果填入为.xx,JVM会在执行java -jar或者java xxx时的工作目录下寻找,比如是/home/howxu/Desktop> java Main,这个动态链接库文件会被指定为/home/howxu/Desktop/xx.
接下来编译Java文件:
1 | |
我们会获得Main.class和A.class文件,还没完,先前提到Java和C语言的数据类型问题,也就是说你直接写一个同名函数是无效的.我们需要专门的头文件和专门的函数声明,按照这个头文件来编写C库,这样才能确保C库中的函数被正确加载,参数正确传递.当然,这一点Java早就考虑到了.
下面的命令要在Main.class,A.class和A.java同时存在的目录下进行(生成头文件的命令是对.java文件进行的,而且同时需要访问到.class文件):
1 | |
特别地,如果你的类文件是包含在一个package里的,你需要用-classpath参数指定整个项目编译出的class文件目录.
如果你使用jdk8以上,应该使用
1 | |
会获得一个A.h文件,接下来写的C库就和这个东西有关了.
C部分
先来看一下生成的A.h文件的内容:
1 | |
首先,导入了jni.h这个头文件,这个头文件是JDK带有的,一会儿编译的时候需要指定参数带上去
extern "C",表明以下内容会按照C语言标准格式进行编译.
JNIEXPORT jint JNICALL Java_A_add (JNIEnv *, jobject, jint, jint);这个显然就是add函数的声明了。
可以看到这里做了很多更改,首先JNIEXPORT,这个符号在JNI.h中定义为#define JNIEXPORT __declspec(dllexport),大体含义指的是这是一个外部导出的函数,可以交付给其他项目使用.
jint,JNi定义的int类型,本质是long.
JNICALL,本质是__stdcall,一种函数调用参数的约定,表明函数参数从右到左保存.(暂时不知道有什么用)
Java_A_add对add函数重命名,格式想必是Java+Java内路径+函数名.
JNIEnv,一个指向JNI运行环境的指针,后面我们会看到,我们通过这个指针访问JNI函数.
jobject,指的是A这个类,我们用它代替this.
最后两个jint就是参数了.
接下来简单实现一下这个函数就行了.
1 | |
编译成dll文件(使用Mingw,powershell,确保你的JAVA_HOME环境变量正确):
1 | |
这样就可以获得A.dll文件
运行
确保A.dll,A.class,Main.class在同一目录,看看效果:
1 | |

符合预期
干票大的
既然说了JNI也可以实现函数,不妨试试写一个冒泡排序出来,这里就不解释了,直接上代码(Java中传递数组是地址传递):
Main.java:
1 | |
A.java:
1 | |
A.c:
1 | |
结果:
