本章的内容是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。
本章完。