Java核心技术卷II(第8版) – 读书笔记 – 第12章

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

本章完。

 

 

 

 

 

 

Leave a Reply

Your email address will not be published.