Java通过JNI调用C/C++动态链接库之HelloWorld

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
2
3
4
5
6
7
8
9
10
//文件名为hello.java
public class hello
{
public native void helloWorld(); //声明本地库中的函数
public static void main(String[] args) {
System.loadLibrary("helloWorld"); //载入本地库
hello t = new hello();
t.helloWorld(); //调用本地库中的函数
}
}

注意,如果本地库中有多个函数,只需要调用一次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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
/* DO NOT EDIT THIS FILE - it is machine generated */
#include <jni.h>
/* Header for class hello */

#ifndef _Included_hello
#define _Included_hello
#ifdef __cplusplus
extern "C" {
#endif
/*
* Class: hello
* Method: helloWorld
* Signature: ()V
*/
JNIEXPORT void JNICALL Java_hello_helloWorld
(JNIEnv *, jobject);

#ifdef __cplusplus
}
#endif
#endif

可以看到在第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
2
3
4
5
6
7
#include "hello.h"
#include <iostream>

JNIEXPORT void JNICALL Java_hello_helloWorld(JNIEnv *env, jobject obj)
{
std::cout << "Hello world!" << std::endl;
}

代码非常简单,只是需要注意函数的声明与原头文件中完全一致

编译并运行

编译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
2
3
4
5
6
7
8
9
10
11
package myjni;

public class hello
{
public native void helloWorld();
public static void main(String[] args) {
System.loadLibrary("helloWorld");
hello t = new hello();
t.helloWorld();
}
}

编译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
2
3
4
5
6
7
#include "include/myjni_hello.h"
#include <iostream>

JNIEXPORT void JNICALL Java_myjni_hello_helloWorld(JNIEnv *env, jobject obj)
{
std::cout << "Hello world!" << std::endl;
}

除了修改头文件之外,其他都不需要变动

编译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
2
3
4
5
Exception in thread "main" java.lang.UnsatisfiedLinkError: no helloWorld in java.library.path
at java.lang.ClassLoader.loadLibrary(ClassLoader.java:1867)
at java.lang.Runtime.loadLibrary0(Runtime.java:870)
at java.lang.System.loadLibrary(System.java:1122)
at hello.main(hello.java:13)

应该是你运行程序时没有指定库路径,注意指定库路径的那个命令行参数必须在所运行的java程序的前面
或者是你生成的动态链接库名称不是libhelloWorld.so

参考

1.Java Native Interface (JNI)
2.Wikipdeia-Java Native Interface
3.Oracle-JNI