不使用IDE做一次JNI开发

2,007 阅读4分钟

通常,在开发 JNI 程序时,我们都会使用 IDE,例如 Eclipse , Android Studio , 这是因为工具简化了开发的流程,提升了工作效率,但是却让我们越来越看不清本质的东西。因此,本篇文章就不使用 IDE 来做一次 JNI 开发,这样我们就可以对原生 JNI 有个更全面的了解。

本文的例子是在 Ubuntu 16.04 下运行的。

新建项目

首先我们新建一个项目目录叫 JNIDemo,然后进入这个目录

bxll:~$ mkdir JNIDemo
bxll:~$ cd JNIDemo/

JNIDemo 目录下,新建一个叫 Hello.java 的文件,代码内容如下

package com.bxll.jnidemo;

public class Hello {
    static {
        System.loadLibrary("hello_jni");
    }

    static native String helloFromJNI();

    public static void main(String[] args) {
        System.out.println(helloFromJNI());
    }
}

首先,在静态代码块中,通过 System.loadLibrary() 方法加载一个名为 hello_jni 的库,在 Linux 平台下,这个库的全名叫做 libhello_jni.so,在 Windows 平台下,这个库的名字叫做 hello_jni.dll。由于我使用的是 Ubuntu,因此一会编译这个库的名字就 必须 为 libhello_jni.so

然后,定义了一个 native 方法 helloFromJNI(),这个方法需要在动态库中实现,这个稍后就会看到。

最后,在 main() 方法中,调用这个 native 方法,并输出这个方法的返回结果。

编译Java文件

在编译 Hello.java 文件之前,我们需要创建一个存放字节码文件的目录,这个目录暂且就叫做 classes

bxll:~/JNIDemo$ mkdir classes

然后,我们把编译生成的字节码文件输出到这个目录

bxll:~/JNIDemo$ javac -d classes/ Hello.java

javac-d 参数表示输出的目录,更多参数请参考 javac

生成头文件

在执行生成头文件操作之前,我们必须要搞清楚一个问题,那就是为何要生成头文件? 因为头文件中声明的函数和Java文件中声明的native方法是一一对应的关系,虚拟机会自动帮我们建立这层联系,这也称之为静态注册。

在生成头文件之前,我们需要创建一个存放头文件的目录jni

bxll:~/JNIDemo$ mkdir jni

然后把生成头文件的目录指定为 jni

bxll:~/JNIDemo$ javah -classpath classes/ -d jni/ com.bxll.jnidemo.Hello

javah-classpath 参数指定字节码的目录,就是我们刚才编译文件所指定的目录,-d 参数指定头文件生成的目录,最后的 com.bxll.jnidemo.Hello 指定字节码文件的全路径。更多的 javah 命令参数请参考 javah

ok, 现在头文件生成了,我们现在来看看它的内容吧,简化版的内容如下

// com_bxll_jnidemo_Hello.h
/*
 * Class:     com_bxll_jnidemo_Hello
 * Method:    helloFromJNI
 * Signature: ()Ljava/lang/String;
 */
JNIEXPORT jstring JNICALL Java_com_bxll_jnidemo_Hello_helloFromJNI
  (JNIEnv *, jclass);

我们只需要关心这个函数原型(其他都是C/C++相关的事),因为Java_com_bxll_jnidemo_Hello_helloFromJNI 这个函数就是对应的 Hello.java 中的 native 方法 helloFromJNI()

另外呢,我们还可以看到有三行注释,第一个注释 Class: com_bxll_jnidemo_Hello 指明了这个函数与哪个 Java类 相关,第二个注释 helloFromJNI 指明是实现了哪个 native 方法,第三个参数 ()Ljava/lang/String; 代表Java类的native方法在JNI中的签名。

JNIEXPORTJNICALL 都是宏,因为不同平台调用动态库中的方法有不同的规范,而这两个宏就是为了做兼容处理,在 Linux 平台,这两个宏其实没有什么用,因为这两个宏都是定义为空的。

实现头文件

既然从头文件中已经了解到函数原型,那么就好实现了

// com_bxll_jnidemo_Hello.cpp

#include "com_bxll_jnidemo_Hello.h"

extern "C" JNIEXPORT jstring JNICALL Java_com_bxll_jnidemo_Hello_helloFromJNI (JNIEnv * env, jclass clazz)
{
    const char * str_hello = "Hello from C++";
    return env->NewStringUTF(str_hello);
}

这里涉及到 JNI 如何生成字符串的,这里暂不做详述。

编译动态库

既然我们已经实现了底层函数,就需要将这些打包成库,以方便 Java 层加载。我们选择把源文件打包成动态库,但是在执行这个动作之前,我们必须保证操作系统的 Java开发环境已经部署妥当,最好也设置了 JAVA_HOME 环境变量,先看下我的 JAVA_HOME 环境变量

bxll:~/JNIDemo$ echo $JAVA_HOME
/usr/lib/jvm/java-8-openjdk-amd64/

那么,现在来生成动态库吧

bxll:~/JNIDemo$ g++ -I $JAVA_HOME/include -I $JAVA_HOME/include/linux -fPIC -shared jni/com_bxll_jnidemo_Hello.cpp -o jni/libhello_jni.so

g++-I 参数指明 JNI 头文件的位置,-fPIC 表示变成成与位置无关的独立代码,-shared 表示编译成动态库,-o 指明生成动态库的目录以及名字,在 Linux 系统下,动态库的名字的形式为 libXXX.so

运行程序

现在动态库都生成了,那么可以运行程序了吗?我们试一下

bxll:~/JNIDemo$ java -classpath classes/ com.bxll.jnidemo.Hello
Exception in thread "main" java.lang.UnsatisfiedLinkError: no hello_jni in java.library.path
	at java.lang.ClassLoader.loadLibrary(ClassLoader.java:1867)
	at java.lang.Runtime.loadLibrary0(Runtime.java:870)
	at java.lang.System.loadLibrary(System.java:1122)
	at com.bxll.jnidemo.Hello.<clinit>(Hello.java:6)

java.lang.UnsatisfiedLinkError 就是告诉你动态库没有链接上,并且后面有说明原因no hello_jni in java.library.path,告诉你 java.library.path 没有发现名为 hello_jni 的动态库,在 Linux 平台下,也就是没有发现 libhello_jni.so 库。

既然我们已经知道原因是在 java.library.path 属性所指定的目录下没有找到库,那么我可以把生成的库放到这个指定路径下,这样就可以了吧。没错,确实可以,但是这样未免太麻烦,在 Linux 平台下,可以把库的路径加入到 LD_LIBRARY_PATH 环境变量中,程序也会在这个路径下搜索库。

由于我的开发环境中暂时还没有定制自己的库,因此 LD_LIBRARY_PATH 这个环境变量为空,那么现在我们设置下

bxll:~/JNIDemo$export LD_LIBRARY_PATH=./jni/

我们把库的搜索路径指定到了当前目录下的 jni 目录,因为刚才我们把动态库输出到这个目录下。

那么,现在再运行这个 Java 程序,你就会看到想要的效果

bxll:~/JNIDemo$ java -classpath classes/ com.bxll.jnidemo.Hello
Hello from C++

结果已经说明一切。

延伸阅读

Linux平台下静态库和动态库

cs-fundamentals.com/c-programmi…