Java可以通过JNI(Java Native Interface)来调用本地库,从而解决一些需要使用C/C++来提高效率但却需要使用JAVA调用的场景,例如opencv库编写的图像处理函数,需要使用spark等大数据框架来调用。
关于
演示一个Hello world的C++通过java调用的过程,系统环境为linux,编译工具使用g++,java版本为jdk1.8。
JNI调用C/C++基本步骤很简单:
- java代码中声明带有native修饰的类方法,该native方法只是在java中进行声明,而不进行实现,在需要调用navtive方法之前进行system.loadLibrary(“xxx”),然后通过类调用方法xxx即可
- 使用javah从java的class文件生成与native函数相应的头文件
- 通过引用含有native方法声明的头文件,采用C++编写native方法的实现,并将其编译为动态链接库
- 然后正常对java编译并执行即可
下面进行详细分析
Java代码
Java代码如下:
1 | //文件名为hello.java |
注意,如果本地库中有多个函数,只需要调用一次System.loadLibrary
即可
调用库中只需要写库的名字,windows下不需要添加后缀.dll
,linux下不需要添加前面的lib和后缀.so
此时可以直接使用javac hello.java
编译生成class
字节码,因为此时实际上java编译器并不会去查看是否已经有了函数helloWorld
的实现。
生成头文件
运行以下命令生成头文件
1 | javah hello |
很多教程中提及此命令执行之前需要先使用javac
对代码编译,其实可以直接使用javah来从java源代码生成头文件,javah会自动生成临时的class文件(该class文件不会在源文件夹中保存),然后再生成头文件。我通常是直接生成头文件,最后再编译java源代码为class,以避免虽时可能需要修改java源代码。
注意,不需要添加后缀.java
,因为它实际上是从class文件生成的头文件,然后文件夹中会生成头文件hello.h
,内容如下:
1 | /* DO NOT EDIT THIS FILE - it is machine generated */ |
可以看到在第15、16行有一个名为Java_hello_helloWorld
的函数声明,其中名称以Java开头,包含了包名、类名和函数名,并以下划线分隔,形如Java_{package_and_classname}_{function_name}(JNI arguments)
。后面编写C/C++代码时的函数名字必须与此处一样。
其中的2个参数作用是:
JNIEnv*
:用于引用JNI环境,该指针变量可以访问所有JNI函数jobject
:引用this
Java对象,也就是可以用来访问当前java调用者
注意该函数被extern "C"
包围着,是为了告诉C++编译器编译时采用C风格的函数命名协议,而不是C++风格的函数命名协议。因为C++为了支持函数重载,编译时采用一种叫做mangling
的方式为每一个重载函数命命名。详细信息可以参见我的另一篇文章C/C++拾遗之extern "C"
该头文件中引用了一个java的头文件jni.h
,该头文件所在目录为$JAVA_HOME/include
,也就是你的JDK安装目录下面。实际上根据平台的不同,jni.h
头文件中还引用了一个名为jni_md.h
的头文件,该文件目录为$JAVA_HOME/include/linux
C/C++代码
引用头文件并编写C++代码实现函数helloWorld:
1 |
|
代码非常简单,只是需要注意函数的声明与原头文件中完全一致
编译并运行
编译C++代码为动态链接库
使用以下命令编译C++代码为动态链接库:
1 | g++ -fPIC -shared -o libhelloWorld.so helloWorld.cpp -I/usr/lib/jvm/java-8-oracle/include/ -I/usr/lib/jvm/java-8-oracle/include/linux/ |
注意其中的编译选项:
-fPIC
选项使编译器在编译阶段生成与位置无关的代码,以使共享库能够在内存中被正确加载,PIC即Position, Independent Code
。使用-shared
选项时必须有该选项,否则编译期会出错-shared
编译器生成共享链接库-o
后面的动态链接库的命名规则必须与linux下的动态链接库一致,即libxxx.so
的形式-I
后面跟的是jni所需要的头文件路径
编译完成后会生成名为libhelloWorld.so
的文件- 提示,如果是C代码,使用gcc编译时,需要通过
-Wl,--add-stdcall-alias
向链接器传递链接选项,以避免出现UnsatisfiedLinkError
错误。*
编译java代码
java代码直接使用javac编译即可:
1 | javac hello.java |
运行
注意运行的时候需要手动指定java的库引用路径,或者手动将相应的动态链接库文件拷贝到系统库路径。关于系统库路径的问题,可以参考Linux动态链接库以及链接器相关知识
1 | java -Djava.library.path=. hello |
如果以上过程都没有问题,输出应该是:
1 | Hello world! |
与java包管理结合的JNI
非常简单,正常将java源代码放到相应的包文件夹中,然后重新使用javah生成相应头文件即可。
java代码
新建一个文件夹名为myjni,将hello.java放在该文件夹,并在源代码最前面添加package myjni;
java源代码hello.java
:
1 | package myjni; |
编译java代码:
1 | javac myjni/hello.java |
生成头文件
1 | javah -d include myjni.hello |
javah的-d
参数是指定头文件存储路径
此时发现头文件中的函数名字中包括了包名:
1 | JNIEXPORT void JNICALL Java_myjni_hello_helloWorld(JNIEnv *, jobject); |
修改C++代码包含新的头文件
C++源代码如下:
1 |
|
除了修改头文件之外,其他都不需要变动
编译C++为动态链接库
命令并无变化:
1 | g++ -fPIC -shared -o libhelloWorld.so -I /usr/lib/jvm/java-8-oracle/include/ -I /usr/lib/jvm/java-8-oracle/include/linux/ helloWorld.cpp |
执行
1 | java -Djava.library.path=. myjni.hello |
加上了包名字正常执行即可。
打包为jar并执行
打包命令:
1 | jar -cevf myjni.hello myjni.jar myjni |
jar命令用于java打包,参数意义如下:
c
表示创建jar包e
代表可执行的类,即含有main方法的类,要带上包名v
表示显示详细生成过程f
表示生成的jar包名称
执行jar包:
1 | java -Djava.library.path=. -jar myjni.jar |
同样需要注意添加库路径,且库路径选项需要在执行程序名之前
发现JAVA调用C++库并没有那么复杂,总结来说是只是给java一个函数调用入口即可,具体的函数内部实现可以使用你熟悉的任何C/C++方式进行实现,然后编译成动态链接库给JNI调用就可以了。更多兴趣可以参见进阶内容Java通过JNI调用C/C++动态链接库之参数传递及结果返回
可能遇到的问题
java.lang.UnsatisfiedLinkError
整个错误提示为:
1 | Exception in thread "main" java.lang.UnsatisfiedLinkError: no helloWorld in java.library.path |
应该是你运行程序时没有指定库路径,注意指定库路径的那个命令行参数必须在所运行的java程序的前面
或者是你生成的动态链接库名称不是libhelloWorld.so
参考
1.Java Native Interface (JNI)
2.Wikipdeia-Java Native Interface
3.Oracle-JNI