Java JNI开发

Java JNI开发

JNI,即Java Native Interface,字面意思“Java本地接口”,这里的本地接口,指的就是c/c++开发的接口。由于JNI是JVM规范的一部分,因此我们写的JNI程序可以在任何实现了JNI规范的Java虚拟机中运行(跨平台)。这里先粗略了解一下Java的JNI开发,为学习Android NDK开发做准备。

为什么要JNI开发

  • 标准Java类库不支持与平台相关的应用程序所需的功能;
  • 已经拥有了一个用另一种语言实现的库,而有希望能通过JNI使Java代码能够访问该库;
  • 想利用低级语言(如汇编语言)来实现一小段限制代码。

JNI编程能干什么

  • 创建、检查及更新Java对象(包括数组和字符串);
  • 调用Java的方法;
  • 捕捉和抛出异常;
  • 加载和获得类信息;
  • 执行运行时类型检查;
  • 与Java Api调用一同使用,使得任意本地应用程序可以潜入到Java虚拟机中。

JNI开发流程

JNI开发流程的主要步骤:

  1. 编写声明了native方法的Java类;
  2. 将Java源代码编译成class字节码文件;
  3. 用javah -jni命令生成.h头文件(javah是jdk自带的一个命令,-jni表示将class中用native声明的函数生成jni规则的函数)
  4. 用本地代码实现.h头文件中的函数(即用c或c++编写.h头文件中声明的方法的实现)
  5. 将本地代码编译成动态库(Windows环境下编译成*.dll文件;Linux/Unix下编译成*.so;Mac下编译成*.jniLib)
  6. 拷贝动态库只java.library.path本地库搜索目录下,运行Java程序即可;

示例

下面将以一个HelloWorld的实例来说明上述过程

本机环境macOs High Sierra 10.13,编辑器IDE采用的是IntelliJ IDEA 2016.3.7

编写声明了native方法的Java文件

HelloWorld.java

package com.rainmonth;

import java.io.IOException;

import static com.rainmonth.Utils.addLibraryDir;

public class HelloWorld {

    public static void main(String[] args) throws IOException {

    }

    public native String sayHello(String name);
}

上面定义了一个native方法sayHello

编译生成class字节码文件

javac -d out/production/JniLearn/ src/com/rainmonth/HelloWorld.java

运行上面命令,即可在项目根目录的bin/com/rainmonth生成HelloWorld.class文件注意:bin如果之前不存在,需要手动创建,不然会提示找不到目录:bin

温馨提示:可以直接输入javac查看该命令的使用帮助

生成对应的.h头文件

这一步需要用到javah命令,先看看该命令的用法,终端输入javah

用法: 
  javah [options] 
其中, [options] 包括:
  -o                 输出文件 (只能使用 -d 或 -o 之一)
  -d                  输出目录
  -v  -verbose             启用详细输出
  -h  --help  -?           输出此消息
  -version                 输出版本信息
  -jni                     生成 JNI 样式的标头文件 (默认值)
  -force                   始终写入输出文件
  -classpath         从中加载类的路径
  -cp                从中加载类的路径
  -bootclasspath     从中加载引导类的路径
 是使用其全限定名称指定的
(例如, java.lang.Object)。

所以如果你想在根目录的lib下生成HelloWorld.h文件,输入

javah -o lib/HelloWorld.h -classpath bin/ com.rainmonth.Hello

输入后lib目录下得到文件HelloWorld.h

如果你想采用默认的命名方式,只想输出到lib目录,输入

javah -d lib/ -classpath bin/ com.rainmonth.HelloWorld

输入后lib目录下得到文件com_rainmonth_HelloWorld.h(这是该命令默认生成文件名的命名方式)

注意:上面的-classpath是用来指定存放.class文件的目录的,.class文件在输入是不用带后缀

用本地代码实现.h头文件中的函数

这里采用c实现(也可采用c++实现,二者稍许不同),在lib目录下新建文件HelloWorld.c

// HelloWorld.c

#include "HelloWorld.h"

#ifdef __cplusplus
extern "C"
{
#endif

/*
 * Class:     com_rainmonth_HelloWorld
 * Method:    sayHello
 * Signature: (Ljava/lang/String;)Ljava/lang/String;
 */
JNIEXPORT jstring JNICALL Java_com_rainmonth_HelloWorld_sayHello
  (JNIEnv *, jobject, jstring)
{
    const char *c_str = NULL;
    char buff[128] = { 0 };
    c_str = (*env)->GetStringUTFChars(env, j_str, NULL);
    if (c_str == NULL)
    {
        printf("out of memory.\n");
        return NULL;
    }
    (*env)->ReleaseStringUTFChars(env, j_str, c_str);
    printf("Java Str:%s\n", c_str);
    sprintf(buff, "hello %s", c_str);
    return (*env)->NewStringUTF(env, buff);
}

#ifdef __cplusplus
}
#endif

将本地代码编译成动态库

这一步要用到gcc命令,

上面已经说到了,不同的系统对应的生成命令不同

  • Mac OS

    gcc 
    -dynamiclib 
    -o lib/libHelloWorld.dylib lib/HelloWorld.c 
    -framework JavaVM 
    -I /Library/Java/JavaVirtualMachines/jdk1.8.0_45.jdk/Contents/Home/include/ 
    -I /Library/Java/JavaVirtualMachines/jdk1.8.0_45.jdk/Contents/Home/include/darwin/

    注意,这里只是为了说明才这样写的,实际上命令见用空格隔开即可( -I甚至都不用空格)。

    执行完毕后,lib目录下就得到了libHelloWorld.dylib文件,这就是我们的动态库。

    命令参数讲解

    -dynamiclib:表示要生存动态库

    -o:表示要生成的库文件的名字

    lib/HelloWorld.c:指的是实现代码对应的文件

    -I:表示生成动态库所要引用的头文件(第一个是平台无关的,第二个是平台相关的,可以打开对应文件看看里面到底是什么鬼。

  • Windows

    该平台我尚未尝试,这里贴上别人的说明(以Windows7下VS2012为例

    开始菜单–>所有程序–>Microsoft Visual Studio 2012–>打开VS2012 X64本机工具命令提示,用cl命令编译成dll动态库:

    cl -I"%JAVA_HOME%\include" -I"%JAVA_HOME%\include\win32" -LD HelloWorld.c -FeHelloWorld.dll   

    其中,JAVA_HOME为jdk的安装路径,其他参数选项说明如下:

    -I : 和mac os x一样,包含编译JNI必要的头文件

    -LD:标识将指定的文件编译成动态链接库

    -Fe:指定编译后生成的动态链接库的路径及文件名

  • Linux/Unix

    gcc -I$JAVA_HOME/include -I$JAVA_HOME/include/linux -fPIC -shared HelloWorld.c -o lib/libHelloWorld.so 

    参数说明:

    -I: 包含编译JNI必要的头文件

    -fPIC: 编译成与位置无关的独立代码

    -shared:编译成动态库

    -o: 指定编译后动态库生成的路径和文件名

运行java程序

将第一步中的HelloWorld.java修改如下:

package com.rainmonth;

import java.io.IOException;

public class HelloWorld {

    static {// 采用静态代码引入动态库
        System.loadLibrary("HelloWorld");
    }

    public static void main(String[] args) throws IOException {
        HelloWorld helloWorld = new HelloWorld();
        System.out.println(helloWorld.sayHello("Hello"));
    }

    public native String sayHello(String name);
}

运行结果:

Exception in thread "main" java.lang.UnsatisfiedLinkError: no HelloWorld in java.library.path
    at java.lang.ClassLoader.loadLibrary(ClassLoader.java:1865)
    at java.lang.Runtime.loadLibrary0(Runtime.java:870)
    at java.lang.System.loadLibrary(System.java:1122)
    at com.rainmonth.HelloWorld.(HelloWorld.java:8)
    at java.lang.Class.forName0(Native Method)
    at java.lang.Class.forName(Class.java:264)
    at com.intellij.rt.execution.application.AppMain.main(AppMain.java:123)

意思是,在java.library.path中找不到对应动态库,也就是说动态库的位置可能存在问题。这稍后再解决,我们换一种方式引入动态库,采用绝对路径来引入试试:

package com.rainmonth;

import java.io.IOException;

public class HelloWorld {

    static {// 采用静态代码引入动态库
//        System.loadLibrary("HelloWorld");
        System.load("/Users/RandyZhang/IdeaProjects/JniLearn/lib/libHelloWorld.dylib");
    }

    public static void main(String[] args) throws IOException {
        HelloWorld helloWorld = new HelloWorld();
        System.out.println(helloWorld.sayHello("Hello"));
    }

    public native String sayHello(String name);
}

得到了我们想要的结果

hello Hello
Java Str:Hello

那面上面抛出异常是什么原因引起的呢,然来在java.library.path下找不到我们的动态库文件,这时我们只要将生成的库文件复制到任意一个java.library.path下即可(这个java.library.path可以通过如下方式获取)

public static void main(String[] args) {
    System.out.println(System.getProperty("java.library.path")); 
}

那很快我们就想到了用System.setProperty(“java.library.path”, pathname)来动态设置这个值,很抱歉,在下尝试了,并没有成功,看了相关的代码你会发现,这个值只在JVM启动时初始化一次:

if (sys_paths == null) {
    usr_paths = initializePath("java.library.path");
    sys_paths = initializePath("sun.boot.library.path");
}

你后面通过set的方式没有生效,当然有大神已经通过反射的方式解决了这一问题:

public static void addLibraryDir(String libraryPath) throws IOException {
        try {
            Field field = ClassLoader.class.getDeclaredField("usr_paths");
            field.setAccessible(true);
            String[] paths = (String[]) field.get(null);
            for (int i = 0; i < paths.length; i++) {
                if (libraryPath.equals(paths[i])) {
                    return;
                }
            }

            String[] tmp = new String[paths.length + 1];
            System.arraycopy(paths, 0, tmp, 0, paths.length);
            tmp[paths.length] = libraryPath;
            field.set(null, tmp);
        } catch (IllegalAccessException e) {
            throw new IOException("Failed to get permissions to set library path");
        } catch (NoSuchFieldException e) {
            throw new IOException("Failed to get field handle to set library path");
        }
    }

通过调用这个方法动态的添加库目录到usr_paths,当然局限性就是若表示库目录的变量(即usr_paths)修改了,这种方式就失效了。

我们可以通过配置vmOptions来动态制定java.library.path的值,如:

-Djava.library.path="lib库目录"

具体位置就在Edit Configurations下,或者直接运行命令也可:

java -Djava.library.path=/Users/RandyZhang/IdeaProjects/JniLearn/lib -classpath ./bin/ com.rainmonth.HelloWorld

总结

  1. Mac系统下编译成.so文件时,采用绝对路径加载动态库同样也可以;
  2. 注意设置和修改java.library.path属性的不同方式
  3. 要熟悉Java常用的命令行的使用,具体可以查看相应命令的帮助选项。

你可能感兴趣的:(Java基础,Android学习开发,Linux学习,java,jni,android)