命题作文:对Java Native Interface (JNI) 有一个认识,使用JNI完成一个打印输出的工作。
本文通过回答三个环环相扣的问题,来对JNI的功能,作用做一个说明。
- JNI是干什么的?
- 面对一个问题,比如打印输出,JNI的工作流程是什么,每个步骤的目的是什么?
- 将每个步骤对应的代码写出来,跑出来。
一、JNI是干什么用的
JNI的中文为JAVA本地调用,本机上用其他语言编写并且编译过的代码(在linux上就是so,在windows上就是dll),可以通过这个本地接口(native interface)在java虚拟机(JVM)上运行[1]。
JNI一开始是为了本地已编译语言,尤其是C和C++而设计的,但是它并不妨碍你使用其他语言,只要调用约定受支持就可以了。JNI是JDK 的一部分,用于为Java提供一个本地代码的接口。比如:Javah,产生可以调用Java过程的C过程,或建立能被Java程序调用的C过程的头文件。
JNI使得运行在JVM虚拟机上的Java代码能够操作使用其它语言编写的应用程序和库,比如 C/C++以及汇编语言等。此外JNI提供的某些API还允许你把JVM嵌入到本地应用程序中。
JNI标准至少保证本地代码能工作在任何Java 虚拟机下实现。
使用JNI来整合本地代码和Java代码的步骤是确定的,没有再创作的余地,所以读者可以通过本文的步骤来逐步认识到,其实Java也是"没有什么不可以 "的。
二、JNI的工作机制是什么
如果现有一个java类需要通过JNI来运行用c编写的功能,比如打印输出hello world。那么应该进行如下步骤:
- 编写一个java类,在这个java类中,使用JNI加载需要调用的库函数和这个库函数中的具体方法。
- 编译这个java类。然后运行这个类即可。前提是本地库函数要准备好。但是一般而言,这个本地库函数是没有准备好的,或者说是要现场准备的。所以进入第三步。
- 现在介绍怎么准备java类所要调用的用c语言编写编译过的库函数。
- 为这个用c编写的库函数准备一个头文件,这样可以这个让c编写的库函数与调用这个库函数的java class进行交互。头函数是早期编程语言,包括C语言特有的,用于函数的声明,意思就是说有这么一个函数在,名字叫啥啥啥。所以,在这里,这个头函数里其实就是放了库函数里的方法的声明。
- 开始用C语言编写这个库函数。首先是写成源代码。在这个源代码中要调用为它准备好的头文件。然后编写这个库函数的主要功能,就是打印输出hello world,写好后,编译成库函数。一旦这个库函数准备好后,就可以回到第二步,运行java 类文件,然后就可以打印输出了。
三、以上例子的代码实现
- 编写这个java 类
public class HelloJNI
{
static { System.loadLibrary("hello"); // Load native library at runtime
// hello.dll (Windows) or libhello.so (Unixes)
}
// Declare a native method sayHello() that receives nothing and returns void
private native void sayHello();
// Test Driver
public static void main(String[] args) { new HelloJNI().sayHello(); // invoke the native method
}
}
-
将这个java 类编译成class文件,命令如下:
javac HelloJNI.java
如果库函数已经准备好了,就可以运行这个class文件,得到打印输出的 Hello, World!命令如下:
java HelloJNI
否则,进入第3步: 与第二节的步骤对应起来
把步骤2得到的class进一步处理,得到库函数所需要的头函数。命令如下:
javah HelloJNI
用C语言编写库函数,代码如下:
#include <jni.h>
#include <stdio.h>
#include "HelloJNI.h"
// Implementation of native method sayHello() of HelloJNI class
JNIEXPORT void JNICALL Java_HelloJNI_sayHello(JNIEnv *env, jobject thisObj) {
printf("Hello World!\n");
return;
}
2.1. 将这个编写好的源程序编译成o文件,在linux下的代码是:
gcc -fpIC -I/usr/lib/jvm/java-8-oracle/include -I/usr/lib/jvm/java-8-oracle/include/linux -c HelloJNI.c
其中,/usr/lib/jvm/java-8-oracle/include表示头文件jni.h的所在路径,/usr/lib/jvm/java-8-oracle/include/linux表示头文件jni_md.h所在的路径,/usr/lib/jvm/java-8-oracle表示JDK的安装目录。
2.2. 将这个o文件进一步连接成库函数(so文件),在linux下的代码是:
gcc -o libhello.so -shared HelloJNI.o
2.3. 将so文件所在的路径加入环境变量中去,在linux下的代码是:
export LD_LIBRARY_PATH=/home/cxy/test
然后就可以回到第二步,用java HelloJNI
命令运行class文件即可。
结果如下图所示:
本文结束
Second Try
step1. 填写一个调用C语言的Java类
public class HelloJNI {
static {
System.loadLibrary("hello"); // Load native library at runtime
// hello.dll (Windows) or libhello.so (Unixes)
}
// Declare a native method sayHello() that receives nothing and returns void
private native void sayHello();
// Test Driver
public static void main(String[] args) {
new HelloJNI().sayHello(); // invoke the native method
}
}
- static initializer invokes System.loadLibrary() to load a library.
- the name of the library is hello/Hello. it is a native library.
- the native library "Hello" contains a native method "sayHello()"
- The native librry "Hello" will be mapped to libhello.so in Linux
- This library should be included in Java's library path, otherwise, the program will throw a UnsatisfiedLinkError. You could include the library into Java Library's path
export LD_LIBRARY_PATH=/home/x/Desktop/JNI
- Next, we declare method "sayHello()" as a native instance method. native means this method is implemented in another language. The sayHello() is contained in the native library loaded.
- The main() method allocate an instance of HelloJNI and invoke the native method sayHello(). new HelloJNI class's instance,and invoke the native method sayHello() in it.
- Question? why declare method sayHello as a native instance method? I mean maybe
System.loadLibrary("hello");
does not know it is a native library? or native library constain method that is not native?
Compile the "HelloJNI.java" into "HelloJNI.class".
javac HelloJNI.java
- javac 是java语言编程编译器。全称 javacompilation.javac工具读由java语言编写的类和接口的定义,并将它们编译成字节代码的class文件。My understanding is exe file.
Step2: Create the C/C++ Header file - HelloJNI.h 创建一个C的头文件,命名为HelloJNI.h
Run javah utility on the class file to create a header file for C/C++ programs: javah HelloJNI
- the utility means utility program that is "HelloJNI.class"
- what does headfile use for? The declaration, tell the program the name of the fun. Then, according to the name of the fun, it can find the body.
- give the C file with headfile "HelloJNI.h", it can be inferred that the c file invokes the function declared in "HelloJNI.h". Thus, to see what does the c file invokes, open it.
-
#include <jni.h>
it include a jni.h headfile, -
JNIEXPORT void JNICALL Java_HelloJNI_sayHello(JNIEnv *, jobject);
is a function written by c. -
Java_{package_and_classname}_{function_name}(JNI arguments)
the naming convention for C function 命名规则。 - Arguments is 命令行参数。
- JNIEnv*: reference to JNI environment, which lets you access all the JNI fucntions.
- jobject: reference to "this" Java object.
- The extern "C" is recognized by C++ compiler only. It notifies the C++ compiler that these functions are to be compiled using C's function naming protocol (instead of C++ naming protocol). C and C++ have different function naming protocols as C++ support function overloading and uses a name mangling scheme to differentiate the overloaded functions. Read "Name Mangling".
Step 3: C Implementation - HelloJNI.c
#include <jni.h>
#include <stdio.h>
#include "HelloJNI.h"
// Implementation of native method sayHello() of HelloJNI class
JNIEXPORT void JNICALL Java_HelloJNI_sayHello(JNIEnv *env, jobject thisObj) {
printf("Hello World!\n");
return;
}
- HelloJNI.c 相当于一个子函数,最终目的还是被调用,不同的是是被Java写的程序调用,不是被C写的程序调用。既然要被调用,就要把HelloJNI.c里面的函数的函数名提取出来,放在头文件里。这个函数名就是Java_HelloJNI_sayHello。所以,
JNIEXPORT void JNICALL Java_HelloJNI_sayHello(JNIEnv *, jobject);
是出现在HelloJNI.h这个头文件里面的。 - void is the data type,表示返回值是空值。
- JNIEXPORT is a key word, JNIEXPORT :在Jni编程中所有本地语言实现Jni接口的方法前面都有一个"JNIEXPORT",这个可以看做是Jni的一个标志,至今为止没发现它有什么特殊的用处。与被C语言调用做一个区分标识。
- JNICALL :这个可以理解为Jni 和Call两个部分,和起来的意思就是 Jni调用XXX(后面的XXX就是JAVA的方法名,就是HelloJNI这个类中的方法sayHello())。
-
Java_HelloJNI_sayHello
就是被JNICALL调用的部分。也就是Java中的native 方法名,这里起名字的方式比较特别,是:包名+类名+方法名。
将这段C写的子函数,编译成动态库文件,这样就可以被调用。
step1. gcc -fpic -I/usr/lib/jvm/java-8-oracle/include -I/usr/lib/jvm/java-8-oracle/include/linux -c HelloJNI.c
其中,/usr/lib/jvm/java-8-oracle/include表示头文件jni.h的所在路径,/usr/lib/jvm/java-8-oracle/include/linux表示头文件jni_md.h所在的路径,/usr/lib/jvm/java-8-oracle表示JDK的安装目录。
step2. 将这个o文件进一步连接成库函数(so文件),在linux下的代码是:gcc -o libhello.so -shared HelloJNI.o
step3. 将so文件所在的路径加入环境变量中去,在linux下的代码是:export LD_LIBRARY_PATH=/home/cxy/test
(注意:路径不一样)
step 4:执行
然后就可以回到第二步,用java HelloJNI命令运行class文件即可。
java HelloJNI
执行java class文件,从主函数进去,new一个HelloJNI类,并调用类中的sayHello方法。这个sayHello并没有像一般的java程序,里面定义了执行的代码语句,没有输入也没有输出。既然这样是不是就断了?不是,这个sayHello给出了关键字native,说明是用其他语言执行的。并且这个sayHello的执行部分已经写在了"hello"这个函数库里面。这个函数库:hello:就对应了linux中的动态库函数“libhello.so”。那么再调用这个“libhello.so”就可以继续执行下去。执行这个动态库函数首先去载入函数声明。发现函数声明JNIEXPORT void JNICALL Java_HelloJNI_sayHello(JNIEnv *, jobject);
这个函数声明正好对应了java class “HelloJNI” 中的方法 “sayHello”,也就是说java class中要调用的“sayHello”在这个动态库so中找到了,那就顺着这个声明去找函数body,要给出路径才能找到,找到body就执行这个body,返回空值,java class “HelloJNI” 中的方法 “sayHello”获得空值,执行完毕。