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代码如下:
//文件名为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的实现。
生成头文件
运行以下命令生成头文件
javah hello
很多教程中提及此命令执行之前需要先使用javac对代码编译,其实可以直接使用javah来从java源代码生成头文件,javah会自动生成临时的class文件(该class文件不会在源文件夹中保存),然后再生成头文件。我通常是直接生成头文件,最后再编译java源代码为class,以避免虽时可能需要修改java源代码。
注意,不需要添加后缀.java,因为它实际上是从class文件生成的头文件,然后文件夹中会生成头文件hello.h,内容如下:
/* 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,该头文件所在目录为