实践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 |
|
结果: