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!
    

再次测试通过!

整个程序过程如下:

  1. “TestJni.java”调用”HelloJni.java”中的sayHello()方法
  2. “HelloJni.java”调用”libHelloJni.so”中的Java_HelloJni_sayHello()方法,此处由Java调到了C++
  3. “libHelloJni.so”调用”libhello.so”中的hello()方法
  4. “libhello.so”将”Hello JNI!“字样打印在屏幕上

程序是运行正常了,是不是觉得有点小复杂?下一篇,我们将介绍一个更简单的方法JNA来实现同样的功能。

本例代码可以从这里下载