本文讲解一个简单的示例程序:在Java程序中调用原生方法,打印Hello World。让读者大概了解JNI编程的步骤。
对于本系列所有的示例程序,作者均在终端(Terminal)下编写、编译、调试和配置完成的。大家根据个人习惯进行编程。不过我还是说一下本人的编程环境:
1.Mac系统(之前用的Linux,不久前换了Mac,两个系统差不多),Windows系统没有实践过。
2.在执行命令之前,必须确定系统中已经安装了JDK最新版本,gcc和g++等工具。
3.编辑器是终端vim,大家根据习惯可以使用IDE或记事本等。
4.编译器在终端下用命令javac、javah、gcc/g++。
5.所有的配置(基本上是环境变量的设置)均在终端下完成,图形界面没有实践过。
下面提到的内容是用于达成共识:
1.执行命令时,我会这么写:$javac HelloWorld.java。其中’$’是终端命令提示符,不用用户输入,”javac HelloWorld.java”是需要用户输入的命令。
一、概述
JNI编程的步骤大概如下,我们以HelloWorld这个例子来说明:
1.创建Java类HelloWorld.java,类中声明了Java程序要调用的原生方法print(),并编写Java程序。
2.使用javac命令编译Java源文件,生成HelloWorld.class文件。
3.使用javah命令,以HelloWorld为参数生成一个头文件,含有Java应用中声明的原生方法的原型。
4.编写原生方法HelloWorld.c,实现print函数。
5.把HelloWorld.c编译封装成动态库文件,如libHelloWorld.so(Linux)或libHelloWorld.dylib(Mac)。
6.使用java命令运行HelloWorld程序。这时HelloWorld.class文件和生成的动态库都会同时被加载到解释器中执行。
下面贴两张图,用图示说一下步骤:
上面两张图是一个整体,不过太大了,不好截图,就分两张。截图的时候大小不好对齐,凑合着看。
二、实践
上一节讲了JNI编程的流程,下面以HelloWorld程序为例,讲解每一步的代码、命令和配置。
2.1 创建HelloWorld.java
$vim HelloWorld.java #新建Java类。
在类文件中输入以下代码:
class HelloWorld
{
private native void print();
public static void main(String[] args)
{
new HelloWorld().print();
}
static
{
System.loadLibrary(“HelloWorld”);
}
}
这个类中,使用native关键字声明了原生方法print,定义了main方法,在main方法中调用了原生方法print,并在静态上下文中static{…}加载封装了print实现的动态库libHelloWorld.dylib(Mac)或libHelloWorld.so(Linux)或HelloWorld.dll(Windows)。
这个类只是声明了原生方法,原生方法的实现是在单独的HelloWorld.c中。System.loadLibrary();语句用于在Java中加载动态库,它会根据环境变量CLASSPATH中指定的路径查找该动态库。程序员必须保证这个动态库存在于CLASSPATH变量所指定的目录中。
这个程序的运行流程:在运行程序之前,系统首先进入静态上下文,加载动态库(这个动态库后面会编译封装好),然后执行main方法,调用原生方法print,该原生方法已经被封装到了动态库中,因此调用动态库中的相关代码执行功能,返回结果,整个程序退出。
2.2编译HelloWorld.java
$javac HelloWorld.java #当前目录会生成HelloWorld.class
2.3 生成HelloWorld.h
$javah HelloWorld #当前目录会生成HelloWorld.h头文件
这个头文件中声明了原生方法的原型。
自动生成的HelloWorld.h中声明的原型:
JNIEXPORT void JNICALL Java_HelloWorld_print(JNIEnv *, jobject);
我们在HelloWorld.java中声明的原型:
private native void print();
JNIEXPORT是JNI定义的宏,用于让该原生方法能够在动态库中展现出来,外面的程序能够找到这个函数。JNICALL是另一个宏,用于规定调用函数的规范,如函数参数的进栈顺序等。
两个原型一比较,除了两个宏之外,函数名和函数参数发生了变化,返回值没变。这里简单提一下,JNI原生方法的名称规范是Java_完整的类名_方法名。参数中,JNIEnv是JNI接口的入口指针,每个JNI接口必须指定其为第一个参数;jobject是该原生方法所在对象的引用。这里可以忽略这些,后面章节会详细讲解。
2.4编写HelloWorld.c,实现原生方法
$vim HelloWorld.c #新建.c文件
在打开的文件中输入以下代码:
#include <jni.h>
#include <stdio.h>
#include “HelloWorld.h”
JNIEXPORT void JNICALL Java_HelloWorld_print(JNIEnv *env, jobject obj)
{
printf(“Hello World!n”);
return;
}
原生方法很简单,只打印了信息。我们可以从HelloWorld.h中复制原生方法的原型,粘贴在.c中即可,不用自己动手写。
jni.h:这个头文件中声明和定义了很多JNI的接口和宏、数据结构等,所以必须要包含这个头文件。如果报错找不到这个头文件,那么参照2.7节设置环境变量即可。
stdio.h:用到了printf函数,其实jni.h中已经包含了stdio.h,可以省略。
HelloWorld.h:上一步生成的头文件,声明了原生方法的原型。
2.5编译原生方法,封装成动态库
$gcc -fpic -c HelloWorld.c #此命令仅限于Mac和Linux,在当前目录生成HelloWorld.o
$gcc -shared -o libHelloWorld.dylib HelloWorld.o #生成动态库,Linux改为libHelloWorld.so,在当前目录生成libHelloWorld.dylib或.so文件。
2.6运行程序
$java HelloWorld
输出:Hello World
如果没有找到libHelloWorld.dylib或.so文件的话,会出现如下错误:
java.lang.UnsatisfiedLinkError: no HelloWorld in library path
at java.lang.Runtime.loadLibrary(Runtime.java)
at java.lang.System.loadLibrary(System.java)
at HelloWorld.main(HelloWorld.java)
这时只要设置一下环境变量即可,参见2.7节。
2.7设置环境变量
以下命令适用于Mac或Linux,Windows设置环境变量的方法自行搜索。
$vim ~/.bash_profile
1.如果找不到jni.h头文件,说明系统在环境变量中找不到JDK的安装目录,在打开的文件中添加:
export JAVA_HOME=path_to_jdk
一般情况下,Linux的JDK安装路径是/usr/local/jdk-xxx,Mac是/Library/Java/JavaVirtualMachines/jdkxxx.jdk/Contents/Home。xxx代表版本号。
export C_INCLUDE_PATH=$JAVA_HOME/include:$JAVA_HOME/include/darwin
如果是在Linux系统中,则darwin换成linux即可。
2.找不到动态库,在打开的文件中添加:
export JRE_HOME=$JAVA_HOME/jre
1中提到的JAVA_HOME也要添加进来。
export CLASSPATH=.:$JAVA_HOME/lib:$JRE_HOME/lib
注意,要包含当前目录 “.”
export LD_LIBRARY_PATH=.:$JRE_HOME/lib/server:/usr/lib
这样,环境变量就设置完了,可以输入命令:
$source ~/.bash_profile
刷新配置,也可以关闭当前终端,重新打开终端更新配置。更新完成后,再次编译、运行就没有问题了。
3.总结
第一章和第二章是对JNI的简单介绍,最后用一个示例说明JNI编程的流程。基础知识到这里就讲完了,后面的章节会涉及更深入一点的内容。