Java虽然功能强大,但是存在一个缺点,就是JVM本身也是一个动态链接库(内部有class文件的解释器),它加载类和解释执行的效率不如直接编译的C++高。再有就是,Java设计系统API等底层操作时可能无能为力。一些经常调用的函数,或者和操作系统交互的函数必须用其他语言来完成。
Java中的JNI(Java Native Interface)就是实现native方法的途径。它通过C/C++的编程接口(头文件)来达到和C/C++交互的目的。
我们先来看一下,native方法的执行过程。
首先,在类被加载时,需要加载native方法实现的动态链接库,因此这段加载代码必须是静态加载器中的程序段。比如下面的ExampleClass类:
public class ExampleClass {
static {
System.load("example.dll");
}
public native void example();
}
然后,当JVM执行到native函数时,查找已经加载好的动态链接库,如果找到对应函数的实现,则把执行权转交操作系统,操作系统将进程调度至动态链接库,开始函数执行,Java程序则等待其返回值;如果未找到则报错(属于Error型异常,也就是JVM级别的异常,不可捕获)。
了解到这一点,我们就可以编写自己的native方法了。程序员出生时说的第一句话(滑稽)是Hello World,所以我们也以Hello World为例。环境如下:
操作系统:windows 10 64bit
Java IDE:IDEA 2018.3–64bit(eclipse的小伙伴自行对应)
JDK:1.8(AMD64)
C++ IDE:codeblocks 17
C++ compiler:mingw64-g++-8.1(注意,codeblocks自带的编译器是32位的,生成的动态链接库也是32位的,不能被64位的JVM加载。所以稍后我们将手动修改codeblocks的编译器路径)
我们首先新建Java工程,编写一个简单的Java程序,直接在主类中就行。如下:
public class Main {
static {
System.load("D:\\jni.dll");
}
public native static void hello(); // 必须有static,因为静态函数不能直接调用同一个类的非静态函数
public static void main(String[] args) {
hello();
}
}
这个hello函数就是我们要实现的native函数,功能是输出字符串“Hello World”。在这个主类中,需要加载D:\jni.dll
,所以把它写在static初始化器中。
接下来,我们需要编译出class文件,并生成一个C++的头(.h)文件。我们单击idea的一键编译运行即可。这次运行是一定会报错的,提示无法加载动态链接库。但我们不需要运行,只需要那个class文件。
找到你的class文件所在位置。比如,我的是在工程目录\out\production\doorsymbol下,其中doorsymbol是我的工程名。eclipse的小伙伴自行找。反正看到Main.class就对了。如下图:
javah -jni Main
之后便会看到一个Main.h文件。这一步算大功告成。
这一步我们将生成dll文件。我们用codeblocks新建一个C++工程。注意,这次不是控制台程序,而是动态链接库了。工程目录不要有空格,比如我选择D:\cppprojects\jni
这一步一定要选这个:
刚才提到,codeblocks自带的编译器是mingw32,无法编译64位的动态链接库。所以我们需要把默认编译器路径改成mingw64所在的路径。没有mingw64的小伙伴可以下载这里的压缩包并解压。链接:https://pan.baidu.com/s/1jsxoCnntqCo4xRBIFF1ACw
提取码:tian
解压后,转到mingw64文件夹中。如图:
codeblocks–settings–compiler
点可执行工具链(executable tool chain)选项卡,把编译器的安装目录改成刚才的mingw64文件夹。
接着,修改编译选项。单击compiler settings选项卡。选择遵循c++11规范,以及生成x86_64目标。尤其是生成64位目标至关重要,这将直接决定我们生成动态链接库的格式。
然后再修改调试器路径,也就是gdb。如下图:
settings–debugger settings
单击default选项卡,把调试器路径(executable path)改成刚才的mingw64文件夹下的bin/gdb.exe即可。单击确定。
编译器设置好之后,我们的工程中,有一个默认的main.cpp文件和main.h文件。其中main.h文件没有任何用处,把它删掉。我们需要用的是刚才用class文件生成的Main.h。打开工程目录,把这个头文件移动到和main.cpp同级目录下。然后,打开你的jdk路径(以下简称java_home
),如下图:
%java_home%\include\jni.h
%java_home%\include\win32\jni_md.h
最终的效果如下图
打开Main.h,发现它声明了一个函数Java_Main_hello
。这个函数就是Java中native函数hello的真正接口。另外,我们看到头文件包含了#include
,我们把它改成#include "jni.h"
。
然后打开main.cpp文件,删除全部内容,改成如下:
#include "Main.h"
#include
using namespace std;
JNIEXPORT
void JNICALL Java_Main_hello(JNIEnv *, jclass)
{
cout << "Hello World!" << endl;
}
注意一定要包含头文件Main.h,并且函数头必须与Main.h中的声明完全相同。为避免出错,建议复制粘贴。
这一切都完毕之后,单击编译按钮,如果没有出错,则生成动态链接库成功。我们到工程目录下bin\Debug中,找到一个dll文件。把它复制到D盘根目录下,并改名为jni.dll
。
返回IDEA,再次运行Java程序,可见输出Hello World!
字样。这样,我们的native函数就圆满成功了。
思考:如果一个类有多个native函数是什么情况?如果有多个含native函数的类,又是什么情况?
答案:一个类多个native,只生成一个头文件,但头文件中有多个函数声明。多个含native的类,则每个类分别生成一个头文件。