本章的内容是JNI(Java Native Interface),即从Java中访问本地方法(其他语言)。
1、当需要在Java中嵌入其他语言编写的代码,如C时,后者称为本地代码。
2、一般来说,Java虽然在单纯运算方面效率慢于C等,但这往往不是性能瓶颈。例如密码运算在某C/S中占用的时间为10%,网络、I/O速度占90%,用C比Java快两倍,则speedup只有1+0.1*2=1.2,只提升了20%(阿姆达尔定律)。
3、当然,如果项目是遗留的,之前已经有了大量,无法被迁移的C++代码时,也只能使用JNI了。
4、Java平台提供了用于和本地C代码进行交互,称为Java本地接口(JNI)。注意:JNI不支持Java类与C++类之间的任何映射!即使你用C++编写代码,也只能使用它的C子集!
5、下面是一个很俗的例子,你已经猜到了。。。Hello JNI。。。
(1) 在Java中先定义好需要JNI的函数,使用关键词native,这里的static是无所谓的,只是这个例子偷懒而已。
public class JNITest {
public static native void Hello();
}
(2)然后需要根据上述的.java文件生成.h文件,规则很复杂哦~
(a)函数名规则:包名_类名_函数名
(b)如果有重载,需要加__然后编号
(c) 如果有非UTF-8字符,还需要用xxxx代替。
好吧,不会有人这么自虐的,Java为我们提供了自动生成.h头的方法,两步搞定:
javac ./JNITest.java javah JNITest
然后就多出这么一个文件:JNITest.h
/* DO NOT EDIT THIS FILE - it is machine generated */
#include <jni.h>
/* Header for class JNITest */
#ifndef _Included_JNITest
#define _Included_JNITest
#ifdef __cplusplus
extern "C" {
#endif
/*
* Class: JNITest
* Method: Hello
* Signature: ()V
*/
JNIEXPORT void JNICALL Java_JNITest_Hello
(JNIEnv *, jclass);
#ifdef __cplusplus
}
#endif
#endif
(3)然后,我们就可以用这个头文件做为函数原型,编写.c了~
#include "JNITest.h"
#include <stdio.h>
JNIEXPORT void JNICALL Java_JNITest_Hello
(JNIEnv * env, jclass cl) {
printf("Hello, JNI!!\n");
}
(4)编译.so,注意要include两个目录,还要注意生成的so的文件名 lib类名.so
gcc -I/usr/lib/jvm/java-6-sun/include/ -I/usr/lib/jvm/java-6-sun/include/linux/ -fPIC -shared -o libJNITest.so ./JNITest.c
(5)最后在Java代码中,还要加入LoadLibrary:
public class JNITest {
public static native void Hello();
static {
System.loadLibrary("JNITest");
}
public static void main(String[] args) {
Hello();
}
}
(6)运行的时候,需要:
export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:.
或者:
java -Djava.library.path=. JNITest
我比较喜欢后面这种方法,恩。
至此,就结束了这个恶俗的例子。。
结果是:
liheyuan@liheyuan-desktop:src$ java -Djava.library.path=. JNITest Hello, JNI!!
6、由于Java中类型的长度是定的,int就是4字节,long就是8字节,但是C可不是啊亲。。于是,一些映射类型出现了:
jint:4字节,jlong:8字节,jfloat:4字节,jdouble:8字节。
jboolean:1字节,jbyte:1字节,jchar:2字节,jshort:2字节。
7、下面的例子是Java调用C实现加法,主要是演示参数传递。
Java的代码:
public class JNITest {
public static native double Add(int a, double b);
static {
System.loadLibrary("JNITest");
}
public static void main(String[] args) {
System.out.println(Add(10, 2.5));
}
}
.c的代码:
#include "JNITest.h"
//#include <stdio.h>
JNIEXPORT jdouble JNICALL Java_JNITest_Add
(JNIEnv * env, jclass cls, jint a, jdouble b) {
jdouble res = a + b;
return res;
}
8、JNI中访问字符串略麻烦,下面是一个向Java返回字符串的例子:
JNIExport jstring JNICALL Java_JniTest_GeString(JNIEnv* env, jclass cl) {
jstring jstr;
char greeting[] = "Hello, JNI!\n";
jstr = (*env)->NewStringUTF(env, greeting);
return jstr;
}
上面这个env是函数指针,不是对象哦!
9、如果要反着来,即从C中访问Java的String,需要用GetStringUTFChars或者ReleaseStringUTFChars。
10、虽然C不是OO的,但也可以在JNI代码中访问Java的对象(和之中的实例域),这时就不能用static修饰父了。
11、对象在JNI代码中是jobject类型。
接口封装的比较复杂,步骤是:
(1) GetObjectClass 这只是一个临时引用,旨在方法返回前有效,因此不能直接在class中赋值!它不会返回给Java的。
(2) GetFieldID,这时要指定上面获得的obj、域名、域的数据类型缩写(如Double是D)
(3) GetXXXField() XXX是域类型
(4) SetXXXField() XXX是域类型
12、如果不想每次执行时都调用一次GetObjectClass,可以用NewGlobalRef锁定对象,但还需要DeleteGlobalRef来释放对象。
13、下面是访问对象的实例域的例子:
Java还算正常,只不过JNI函数头不再是static的了。
public class JNITest {
public JNITest(double salary) {
this.salary = salary;
}
public native void raiseSalary(double precent);
public String toString() {
return "Salary: " + Double.toString(salary);
}
private double salary;
static {
System.loadLibrary("JNITest");
}
public static void main(String[] args) {
JNITest jt = new JNITest(2700.0F);
System.out.println(jt);
// Call JNI to raise salary
jt.raiseSalary(5.00F);
System.out.println(jt);
}
}
C的代码要注意:GetObjectClass获得的cls,只是为了获得FieldID,而SetXXXField和GetXXXField都是在obj上操作的,要分清楚!
#include "JNITest.h"
JNIEXPORT void JNICALL Java_JNITest_raiseSalary
(JNIEnv * env, jobject this_obj, jdouble precent) {
// Get Object
jclass cls = (*env)->GetObjectClass(env, this_obj);
// Get FieldID
jfieldID id_salary = (*env)->GetFieldID(env, cls, "salary", "D");
// Get Field
jdouble salary = (*env)->GetDoubleField(env, this_obj, id_salary);
printf("%lf\n", salary);
// Raise salary
salary *= (1+precent/100);
// Set Field
(*env)->SetDoubleField(env, this_obj, id_salary, salary);
}
再来看一下头问件:
/* DO NOT EDIT THIS FILE - it is machine generated */
#include <jni.h>
/* Header for class JNITest */
#ifndef _Included_JNITest
#define _Included_JNITest
#ifdef __cplusplus
extern "C" {
#endif
/*
* Class: JNITest
* Method: raiseSalary
* Signature: (D)V
*/
JNIEXPORT void JNICALL Java_JNITest_raiseSalary
(JNIEnv *, jobject, jdouble);
#ifdef __cplusplus
}
#endif
#endif
14、访问静态域与上面类似,区别是:
(1)需要用GetStaticFieldID、GetStaticXXXField、SetStaticXXXField等函数。
(2)找class的时候,必须用FindClass,而不是GetObjectClass
15、上面的使用GetFieldID的时候,需要一些代码,例如float是D。
byte B
char C
double D
float F
int I
long J(诡异啊)
short S
void V
boolean Z
数组 [
16、可以使用javap产生函数签名,这实际是写入到了字节码(Java虚拟机来读)的:
javap -s -private JNITest
Compiled from "JNITest.java"
public class JNITest extends java.lang.Object{
private double salary;
Signature: D
public JNITest(double);
Signature: (D)V
public native void raiseSalary(double);
Signature: (D)V
public java.lang.String toString();
Signature: ()Ljava/lang/String;
public static void main(java.lang.String[]);
Signature: ([Ljava/lang/String;)V
static {};
Signature: ()V
}
17、也可以从本地代码中调用Java中的方法!
18、从JNI代码中调用本地代码的步骤:
(1)GetObjectClass
(2)mid = GetMedhodID
(3)CallXXXMethod(mid) 这里的XXX是函数的返回类型!
18、调用静态方法的区别:GetStaticMethodID, CallStaticXXMethod
19、JNI代码中,可以用newObject来构造新的Java对象,并指定方法名为<init>的构造函数。
20、从JNI代码中调用变参数的函数,用CallNonVirtualXXXMethod,带有V的版本是变参数的。
21、JNI代码中可以访问Java的数组,GetArrayLength、GetObjectArray、SetObjectArrayElement。
22、C语言中是没有异常处理的,很可能发生越界等错误,但这些在Java中是会抛出异常的。我们需要在JNI代码中用NewObject来创建Throwable子类的对象。或者调用ExceptionOccured来抛出异常。
23、如果需要清空异常,用ExceptionClear来关闭异常
24、可以在JNI代码中创建虚拟机。JNI_CreateJavaVM。
本章完。