JNI技术解析第二章 初探JNI

本文讲解一个简单的示例程序:在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_tech2_1jni_tech2_2上面两张图是一个整体,不过太大了,不好截图,就分两张。截图的时候大小不好对齐,凑合着看。

二、实践

上一节讲了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编程的流程。基础知识到这里就讲完了,后面的章节会涉及更深入一点的内容。