Java调用本地代码(二)—— JNI实现
JNI即Java Native Interface(Java本地接口),是Java标准的访问本地代码的方法。它包含的JDK里面,无需下载其他的jar包即可实现。上一篇中,我们已经使用C语言创建了一个叫”libhello.so”的动态链接库,提供一个hello()
的公有方法。本文将介绍如何使用JNI来实现从Java语言调用这个hello()
方法。
创建Java代理类
以后调用本地函数,只需调用该代理类中的方法即可
- 编写代理类”HelloJni.java”
public class HelloJni {
static {
System.loadLibrary("HelloJni");
}
public native void sayHello(String name);
}
该类会加载本地库”HelloJni”,这个库我们稍后会创建。另外它提供了一个公有的本地方法sayHello()
,这个方法将封装调用”libhello.so”中hello()
方法的功能。这个也将在后面的步骤中实现。
编译”HelloJni.java”
$ javac HelloJni.java
编译后生成”HelloJni.class”文件。
生成动态链接库
我们已经有”libhello.so”了,为何还要生成动态链接库呢?因为JNI无法直接调用原生的动态链接库,我们必须创建一个新的动态链接库,封装原有的公有方法,并开放JNI可识别的公有方法,供JNI调用。那为什么不修改原来”libhello.so”中的代码,使其中的hello()
方法可让JNI识别呢?这个当然可以,但是在大部分情况下,你无法获得动态库的源代码,也最好不要改别人家的代码,不利于将来升级。所以,这里假设我们只有”hello.h”和”libhello.so”两个文件,需创建一个JNI可识别的动态库封装原来的库。
生成头文件”HelloJni.h”
这个很简单,不用写任何代码,使用JDK提供的”javah”命令即可$ javah -jni HelloJni
该命令根据你之前创建的Java类”HelloJni”自动生成C/C++的头文件”HelloJni.h”。打开该文件,你可以看到如下代码
/* DO NOT EDIT THIS FILE - it is machine generated */
#include <jni.h>
/* Header for class HelloJni */
#ifndef _Included_HelloJni
#define _Included_HelloJni
#ifdef __cplusplus
extern "C" {
#endif
/*
* Class: HelloJni
* Method: sayHello
* Signature: (Ljava/lang/String;)V
*/
JNIEXPORT void JNICALL Java_HelloJni_sayHello(JNIEnv *, jobject, jstring);
#ifdef __cplusplus
}
#endif
#endif
该文件声明了一个Java_HelloJni_sayHello()
的公有方法,并声明为供JNI调用。jobject
指向将来调这个函数的Java对象的引用,jstring
指向传入的参数。如果之前”HelloJni.java”中sayHello()
方法声明为静态方法static
,那第二个参数就会变为jclass
。
- 编写”HelloJni.cpp”
我们来实现这个Java_HelloJni_sayHello()
函数,通过其来调用”libhello.so”中hello()
函数
#include "HelloJni.h"
#include "hello.h"
JNIEXPORT void JNICALL Java_HelloJni_sayHello(JNIEnv *env, jobject obj, jstring name)
{
const char* pName = env->GetStringUTFChars(name, NULL);
hello(pName);
env->ReleaseStringUTFChars(name, pName);
}
GetStringUTFChars()
函数将Java中的String
内容转换为C/C++中的const char *
;ReleaseStringUTFChars()
函数会释放之前由GetStringUTFChars()
申请的内存,避免内存泄漏;而hello(pName)
方法即调用”libhello.so”中的hello()
方法。代码中需引入”hello.h”。
编译”HelloJni.cpp”,生成动态链接库
$ g++ HelloJni.cpp -I /opt/java/include -I /opt/java/include/linux -L . -lhello -fPIC -shared -o libHelloJni.so
编译器搜索头文件的路径是通过”-I”参数引入的。本例中JDK安装在/opt/java
下,JNI相关的头文件默认放在JDK安装目录里的/include
和/include/linux
下。”hello.h”和”libhello.so”文件就在当前目录下。如果你的目录结构不一样,需做相应修改。
编译成功后,当前目录自动生成”libHelloJni.so”文件。你可以用”ldd”命令来查看其依赖库是否正常
$ ldd libHelloJni.so
测试JNI调用
我们来写个Java程序测试JNI调用,该程序只需访问第1步中创建的代理类中的方法即可。
- 编写测试程序”TestJni.java”
public class TestJni {
public static void main(String[] args) {
HelloJni hello = new HelloJni();
hello.sayHello("JNI");
}
}
main方法中创建了一个”HelloJni”类的实例,并调用其sayHello()
方法,传入字符串”JNI”。
编译”TestJni.java”
$ javac TestJni.java
本地生成”TestJni.class”文件。本例中”HelloJni.class”存放在当前目录下。如果不是,需通过”-classpath”引入其目录。
运行测试程序
$ javac TestJni
屏幕上打印出了
Hello JNI!
再次测试通过!
整个程序过程如下:
- “TestJni.java”调用”HelloJni.java”中的
sayHello()
方法 - “HelloJni.java”调用”libHelloJni.so”中的
Java_HelloJni_sayHello()
方法,此处由Java调到了C++ - “libHelloJni.so”调用”libhello.so”中的
hello()
方法 - “libhello.so”将”Hello JNI!“字样打印在屏幕上
程序是运行正常了,是不是觉得有点小复杂?下一篇,我们将介绍一个更简单的方法JNA来实现同样的功能。
本例代码可以从这里下载