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开发流程的主要步骤:
- 编写声明了native方法的Java类;
- 将Java源代码编译成class字节码文件;
- 用javah -jni命令生成.h头文件(javah是jdk自带的一个命令,-jni表示将class中用native声明的函数生成jni规则的函数)
- 用本地代码实现.h头文件中的函数(即用c或c++编写.h头文件中声明的方法的实现)
- 将本地代码编译成动态库(Windows环境下编译成*.dll文件;Linux/Unix下编译成*.so;Mac下编译成*.jniLib)
- 拷贝动态库只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] <classes>
其中, [options] 包括:
-o <file> 输出文件 (只能使用 -d 或 -o 之一)
-d <dir> 输出目录
-v -verbose 启用详细输出
-h --help -? 输出此消息
-version 输出版本信息
-jni 生成 JNI 样式的标头文件 (默认值)
-force 始终写入输出文件
-classpath <path> 从中加载类的路径
-cp <path> 从中加载类的路径
-bootclasspath <path> 从中加载引导类的路径
<classes> 是使用其全限定名称指定的
(例如, 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.<clinit>(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
总结
- Mac系统下编译成.so文件时,采用绝对路径加载动态库同样也可以;
- 注意设置和修改java.library.path属性的不同方式
- 要熟悉Java常用的命令行的使用,具体可以查看相应命令的帮助选项。