目录
JNI
JNI(Java Native Interface,Java本地接口),是Java平台中的一个强大特性。应用程序可以通过JNI把C/C++代码集成进Java程序中。
通过JNI,开发者在利用Java平台强大功能的同时,又不必放弃对原有代码的投资;因为JNI是Java平台定义的规范接口,当程序员向Java代码集成本地库时,只要在一个平台中解决了语言互操作问题,就可以把该解决方案比较容易的移植到其他Java平台中。
使用场景:
- Java API可能不支某些平台相关的功能。比如,应用程序执行中要使用Java API不支持的文件类型,而如果使用跨进程操作方式,即繁琐又低效;
- 避免进程间低效的数据拷贝操作;
- 多进程的派生:耗时、耗资源(内存);
- 用本地代码或汇编代码重写Java中低效方法。
1、Java调用C函数
将一个本地方法链接到Java程序中,遵循如下几步:
- 在Java类中声明一个本地方法
- 运行javah以获得包含该方法的C声明的头文件
- 用C实现该本地方法
- 将代码置于共享类库中
- 在Java程序中加载该类库
1.1、在Java类中声明一个本地方法
Java编程语言使用关键字native
表示本地方法,而且很显然,还需要在类中放置一个方法。
关键字native
提醒编译器该方法将在外部定义。当然,本地方法不包含任何Java编程语言编写的代码,而且方法头后面直接跟着一个表示终结的分号。因此,本地方法声明看上去和抽象方法声明类似。
package pers.zhang.jni;
public class HelloNative {
public static native void greeting();
}
在这个特定示例中,本地方法也被声明为static。本地方法既可以是静态的也可以是非静态的,使用静态方法是因为我们此刻还不想处理参数传递。
实际上可以编译这个类,但是在程序中使用它时,虚拟机就会告诉你它不知道如何找到greeting方法,它会报告一个UnsatisfiedLinkError
异常。
1.2、运行javah以获得包含该方法的C声明的头文件
为了实现本地代码,需要编写一个相应的C函数,你必须完全按照Java虚拟机预期的那样来命名这个函数。其规则是:
- 使用完整的Java方法名,比如:
HelloNative.greeting
。如果该类属于某个包,那么在前面添加包名,比如:pers.zhang.jni.HelloNative.greeting
。 - 用下划线(
_
)替换掉所有的点号(.
),并加上Java_
前缀,例如Java_pers_zhang_jni_HelloNative_greeting
。 - 如果类名含油非ASCII字母或数字,如:
_
、$
或是大于\u007F
的Unicode字符,用_0xxxx
来替代它们,xxxx是该字符的Unicode值的4个十六进制数序列。
注意:如果重载了本地方法,也就是说,用相同的名字提供了多个本地方法,那么必须在名称后附加两个下划线,后面再加上已编码的参数类型。例如,如果你有一个本地方法greeting和另一个本地方法greeting(int repeat),那么,第一个称为Java_HelloNative_greeting,第二个称为Java_HelloNative_greeting__I。
实际上,没人会手工完成这些操作。相反,你应该用-h标志运行javac,并提供头文件放置的目录:
javac -h . HelloNative.java
该命令会在当前目录创建一个名为pers_zhang_jni_HelloNative.h
的头文件:
/* DO NOT EDIT THIS FILE - it is machine generated */
#include <jni.h>
/* Header for class pers_zhang_jni_HelloNative */
#ifndef _Included_pers_zhang_jni_HelloNative
#define _Included_pers_zhang_jni_HelloNative
#ifdef __cplusplus
extern "C" {
#endif
/*
* Class: pers_zhang_jni_HelloNative
* Method: greeting
* Signature: ()V
*/
JNIEXPORT void JNICALL Java_pers_zhang_jni_HelloNative_greeting
(JNIEnv *, jclass);
#ifdef __cplusplus
}
#endif
#endif
这个文件包含了函数声明(宏JNIEXPORT
和JNICALL
是在头文件jni.h
中定义的,它们为那些来自动态装载库的导出函数标明了依赖于编译器的说明符)。
1.3、用C实现该本地方法
现在,需要将函数原型从头文件中复制到源文件中,并且给出函数的实现代码。
实现文件pers_zhang_jni_HelloNative.c
如下:
#include "pers_zhang_jni_HelloNative.h"
#include <stdio.h>
JNIEXPORT void JNICALL Java_pers_zhang_jni_HelloNative_greeting (JNIEnv * env, jclass cl)
{
printf("Hello Native World!\n");
}
现在,请先忽略两个宏:JNIEXPORT和JNICALL。你会发现,该函数声明,接受两个参数,而对应的Java代码对该函数的声明没有参数。第一个参数是指向JNIEnv结构的指针;第二个参数,为HelloWorld对象自身,即this指针。
注意:也可以使用C++实现本地方法。然而,那样必须将实现本地方法的函数声明为 extern “C” (可以阻止C++编译器混编方法名)。例如:
extern "C"
JNIEXPORT void JNICALL Java_pers_zhang_jni_HelloNative_greeting(JNIEnv* env, jclass cl)
{
cout << "Hello, Native World!" << end;
}
1.4、 将代码置于共享类库中
首先要知道的是,头文件中的jni.h
位于$JAVA_HOME/include
下,在jni.h
中还引入了一个jni_md.h
,该头文件包含jbyte、jint和jlong的依赖于机器的typedef。
jni_md.h的位置依赖于操作系统:
#mac
$JAVA_HOME/include/darwin
#linux
$JAVA_HOME/include/linux
#win
$JAVA_HOME/include/win32
以上两个头文件的位置,在编译时需要通过-I
参数指定。
将本地C代码编译到一个动态装载库中,具体方法依赖于编译器(本文为mac):
# linxu下Gnu C编译器,编译后为.so
gcc -fPIC -I $JAVA_HOME/include -I $JAVA_HOME/include/linux -shared -o HelloNative.so pers_zhang_jni_HelloNative.c
# windows下的微软编译器,编译后为.dll
cl -I $JAVA_HOME/include -I $JAVA_HOME/include/win32 -LD pers_zhang_jni_HelloNative.c -Fe HelloNative.dll
# mac下Gnu C编译器,编译后为.dylib
gcc -I $JAVA_HOME/include -I $JAVA_HOME/include/darwin -dynamiclib -o HelloNative.dylib pers_zhang_jni_HelloNative.c
# 也可以使用可从http:/ww.cygwin.com处免费获取的Cygwin编程环境。它包含了GNUC编译器和Windows下的UNIX风格编程的库。使用Cygwin时,用以下命令:
gcc -mno-cygwin -D __int64="long long" -I $JAVA_HOME/include -I $JAVA_HOME/include/win32 -shared -Wl,--add-stdcall-alias -o HelloNative.dll pers_zhang_jni_HelloNative.c
注意:Windows版本的头文件jni_md.h含有如下类型声明:typedef _int64 jlong; 它是专门用于微软编译器的。如果使用的是GNU编译器,那么需要编辑这个文件,例如:
#ifdef __GNUC__
typedef long long jlong;
#else
typedef __int64 jlong;
#endif
//或者,和上面的示例那样,使用-D __int64="long long"进行编译
1.5、在Java程序中加载该类库
需要在程序中添加一个加载类库的调用,为了确保虚拟机在第一次使用该类之前就会装在这个库,需要使用静态初始化代码块。
方式一:使用System.load方法
public class HelloNativeTest {
static {
//使用绝对路径加载该类库
System.load("/Users/acton_zhang/J2EE/MavenWorkSpace/java_safe_demo/HelloNative.dylib");
}
public static void main(String[] args) {
HelloNative.greeting();
}
}
Hello Native World!
方式二:使用System.loadLibrary方法
public class HelloNativeTest {
static {
//使用相对路径加载类库
System.loadLibrary("HelloNative");
}
public static void main(String[] args) {
HelloNative.greeting();
}
}
需要注意的是,这个相对路径是java.library.path
,也就是说虚拟机会去该路径下查找类库。查看默认情况下的java.library.path:
public static void main(String[] args) {
System.out.println(System.getProperty("java.library.path"));
}
/Users/acton_zhang/Library/Java/Extensions:/Library/Java/Extensions:/Network/Library/Java/Extensions:/System/Library/Java/Extensions:/usr/lib/java:.
所以,共有两个方法让虚拟机找到类库:
- 将类库复制到默认的java.library.path下
- 执行java代码时,将类库所在的位置设置为java.library.path,例如:
java -Djava.library.path=. HelloNativeTest
1.6、总结
1)、关于Load与LoadLibrary
这里 System.load 方法需要读取 .dylib 的绝对路径,如果使用 System.loadLibrary 方法则需要传相对路径,这里建议大家写绝对路径,肯定不会出错。
2)、java.lang.UnsatisfiedLinkError
同样的 .dylib 文件换到别的项目不好使了,这是因为 JNI 要求 package、.h、.cpp 这一系列文件都是对应的,例如前面是 pers_zhang_jni_HelloNative,那我们后续不管 Java 端、C++ 端还是调用的 main 函数,都需要与此对应,如果出现java.lang.UnsatisfiedLinkError错误,需要检查是不是哪里名字没对上。
3)、JNI初始化与销毁
一些本地代码的共享库必须先运行初始化代码。可以把初始化代码放到JNI_0nLoad
方法中。类似地,如果提供该方法,当虚拟机关闭时,将会调用JNI_OnUnload
方法。它们的原型是:
jint JNI_OnLoad(JavaVM* vm, void* reserved);
void JNI_OnUnload(JavaVM* vm, void* reserved);
JNI_OnLoad方法要返回它所需的虚拟机的最低版本,例如:JNI_VERSION_1_2
。
2、数值参数与返回值
当在C和Java之间传递数字时,应该知道它们彼此之间的对应类型。例如,C也有int和long的数据类型,但是它们的实现却是取决于平台的。在一些平台上,int类型是l6位的,在另外一些平台上是32位的。然而,在Java平台上int类型总是32位的整数。基于这个原因,Java本地接口定义了jint、jlong等类型。
2.1、数值类型对应
Java | C | 字节 |
---|---|---|
boolean | jbolean | 1 |
byte | jbyte | 1 |
char | jchar | 2 |
short | jshort | 2 |
int | jint | 4 |
long | jlong | 8 |
float | floa | 4 |
double | jdouble | 8 |
在头文件jni.h和jni_md.h中这些类型被typedef语句声明为在目标平台上等价的类型:
//jni.h
typedef unsigned char jboolean;
typedef unsigned short jchar;
typedef short jshort;
typedef float jfloat;
typedef double jdouble;
//jni_md.h
typedef int jint;
#ifdef _LP64 /* 64-bit */
typedef long jlong;
#else
typedef long long jlong;
#endif
typedef signed char jbyte;
#endif /* !_JAVASOFT_JNI_MD_H_ */
2.2、示例
使用本地方法打印给定域宽度和精度的浮点数。
Printf1.java:
package pers.zhang.jni;
public class Printf1 {
public static native int print(int width, int precision, double x);
}
生成头文件pers_zhang_jni_Printf1.h:
javac -h . Printf1.java
/* DO NOT EDIT THIS FILE - it is machine generated */
#include <jni.h>
/* Header for class pers_zhang_jni_Printf1 */
#ifndef _Included_pers_zhang_jni_Printf1
#define _Included_pers_zhang_jni_Printf1
#ifdef __cplusplus
extern "C" {
#endif
/*
* Class: pers_zhang_jni_Printf1
* Method: print
* Signature: (IID)I
*/
JNIEXPORT jint JNICALL Java_pers_zhang_jni_Printf1_print
(JNIEnv *, jclass, jint, jint, jdouble);
#ifdef __cplusplus
}
#endif
#endif
C实现文件pers_zhang_jni_Printf1.c:
#include "pers_zhang_jni_Printf1.h"
#include <stdio.h>
JNIEXPORT jint JNICALL Java_pers_zhang_jni_Printf1_print(JNIEnv * env, jclass jclass,
jint width, jint precision, jdouble x)
{
char fmt[30];
jint ret;
sprintf(fmt, "%%%d.%df", width, precision);
ret = printf(fmt, x);
fflush(stdout);
return ret;
}
编译为动态库Printf1.dylib:
gcc -I $JAVA_HOME/include -I $JAVA_HOME/include/darwin -dynamiclib -o Printf1.dylib pers_zhang_jni_Printf1.c
测试文件Printf1Test.java:
public class Printf1Test {
static {
System.load("/Users/acton_zhang/J2EE/MavenWorkSpace/java_safe_demo/src/main/java/pers/zhang/jni/Printf1.dylib");
}
public static void main(String[] args) {
int count = Printf1.print(8, 4, 3.14);
count += Printf1.print(8, 4, count);
System.out.println();
for (int i = 0; i < count; i++) {
System.out.print("-");
}
System.out.println();
}
}
运行结果:
3.1400 8.0000
----------------
3、字符串参数
字符串在这两种语言中很不一样,Java编程语言中的字符串是UTF-16编码点的序列
,而C的字符串则是以null结尾的字节序列
。
JNI有两组操作字符串的函数:
- 一组把Java字符串转换成
modified UTF-8字节序列
- 一组将它们转换成
UTF-l6数值的数组
,也就是说转换成jchar数组
。
注释:标准UTF-8编码和“modified UTF-8”编码的差别仅在于编码大于0xFFFF的增补字符。在标准UTF-8编码中,这些字符编码为4字节序列;然而,在modified UTF-8编码中,这些字符首先被编码为一对UTF-16编码的“替代品”,然后再对每个替代品用UTF-8编码,总共产生6字节编码。这有点笨拙,但这是个由历史原因造成的意外,编写Java虚拟机规范的时候Unicode还局限在16位。
如果C代码已经使用了Unicode,那么可以使用第二组转换函数。另一方面,如果字符串都仅限于使用ASCIⅡ字符,那么就可以使用“modified UTF-8”转换函数。
3.1、C代码访问Java字符串
带有字符串参数的本地方法实际上都要接受一个jstring类型的值,而带有字符串参数返回值的本地方法必须返回一个jstring类型的值。JNI函数将读入并构造出这些jstring对象。
例如,NewStringUTF函数会从包含ASCIⅡ字符的字符数组,或者是更一般的“modified UTF-8”编码的字节序列中,创建一个新的jstring对象。
JNI函数有一些奇怪的调用惯例。例如下面是NewStringUTF函数的一个调用:
JNIEXPORT jstring JNICALL Java_HelloNative_getGreeting(JNIEnv* env, jclass cl)
{
jstring jstr;
char greeting[] = "Hello, Native World\n";
jstr = (*env)->NewStringUTF(env, greeting);
return jstr;
}
有对JNI函数的调用都使用到了env指针,该指针是每一个本地方法的第一个参数。env指针是指向函数指针表的指针。所以,必须在每个JNI调用前面加上(*env)->
,以便解析对函数指针的引用。而且,env是每个JNI函数的第一个参数。
注意:C++中对JNI函数的访问要简单一些。JNIEnv类的C++版本有有一个内联成员函数,它负责帮助我们查找函数指针。例如,可以这样调用NewStringUTF函数:
jstr = env->NewStringUTF(greeting);
注意:这里删除了该调用的参数列表里的JNIEnv指针。
NewStringUTF函数可以用来构造一个新的jstring,而读取现有jstring对象的内容,需要使用GetStringUTFChars
函数。该函数返回指向描述字符串的“modified UTF-8”字符的const jbyte*
指针。注意,具体的虚拟机可以为其内部的字符串表示方法自由地选择编码机制。所以,你可以得到实际的Java字符串的字符指针。因为Java字符串是不可变的,所以慎重处理cost就显得非常重要,不要试图将数据写到该字符数组中。另一方面,如果虚拟机使用UTF-16或UTF-32字符作为其内部字符串的表示,那么该函数会分配一个新的内存块来存储等价的“modified UTF-8”编码字符。
虚拟机必须知道你何时使用完字符串,这样它就能进行垃圾回收(垃圾回收器是在一个独立线程中运行的,它能够中断本地方法的执行)。基于这个原因,你必须调用ReleaseStringUTFChars
函数。
另外,可以通过调用GetStringRegion
或GetStringUTFRegion
方法来提供你自己的缓存,以存放字符串的字符。
最后GetStringUTFLength
函数返回字符串的“modified UTF-8”编码所需的字符个数。
3.2、常用方法
- jstring NewStringUTF(JNIEnv* env, const char bytes[]): 根据以全0字节结尾的
modified UTF-8
字节序列,返回一个新的Java字符串对象,或者当字符串无法构建时,返回NULL。 - jsize GetStringUTFLength(JNIEnv* env, jstring string):返回进行UTF-8编码所需的字节个数(作为终止符的全0字节不计入内)。
- const jbyte* GetStringUTFChars(JNIEnv* env, jstring string, jboolean* isCopy):返回指向字符串的
modified UTF-8
编码的指针,或者当不能构建字符数组时返回NULL。直到ReleaseStringUTFChars
函数调用前,该指针一直有效。isCopy指向一个jboolean,如果进行了复制,则填入JNI_TRUE
,否则填入JNI_FALSE
。 - void ReleaseStringUTFChars(JNIEnv* env, jstring string, const jbyte bytes[]):通知虚拟机本地代码不再需要通过bytes(GetStringUTFChars返回的指针)访问Java字符串。
- void GetStringRegion(JNIEnv* env, jstring string, jsize start, jsize length, jchar* buffer):将一个UTF-16双字节序列从字符串复制到用户提供的尺寸至少大于2*length的缓存中。
- void GetStringUTFRegion(JNIEnv* env, jstring string, jsize start, jsize length, jbyte* buffer):将一个
modified UTF-8
字符序列从字符串复制到用户提供的缓存中。为了存放要复制的字节,该缓存必须足够长。最坏情况下,要复制3*length个字节。 - jstring NewString(JNIEnv* env, const jchar chars[], jsize length):根据Unicode字符串返回一个新的Java字符串对象,或者在不能构建时返回NULL。
- jsize GetStringLength(JNIEnv* env, jstring string):返回字符串中字符的个数。
- const jchar* GetStringChars(JNIEnv* env, jstring string, jboolean* isCopy):返回指向字符串的Unicode编码的指针,或者当不能构建字符数组时返回NULL。直到
ReleaseStringChars
函数调用前,该指针一直有效。isCopy要么为NULL;要么在进行了复制时,指向用JNI_TRUE
填充的jboolean,否则指向用JNI_FALSE
填充的jboolean。 - void ReleaseStringChars(JNIEnv* env, jstring string, const jchar chars[]):通知虚拟机本地代码不再需要通过chars(GetStringChars返回的指针)访问Java字符串。
3.3、示例
编写一个调用C函数springf的类Printf2.java
package pers.zhang.jni;
public class Printf2 {
public static native String sprint(String format, double x);
}
生成头文件pers_zhang_jni_Printf2.h:
javac -h . Printf2.java
/* DO NOT EDIT THIS FILE - it is machine generated */
#include <jni.h>
/* Header for class pers_zhang_jni_Printf2 */
#ifndef _Included_pers_zhang_jni_Printf2
#define _Included_pers_zhang_jni_Printf2
#ifdef __cplusplus
extern "C" {
#endif
/*
* Class: pers_zhang_jni_Printf2
* Method: sprint
* Signature: (Ljava/lang/String;D)Ljava/lang/String;
*/
JNIEXPORT jstring JNICALL Java_pers_zhang_jni_Printf2_sprint
(JNIEnv *, jclass, jstring, jdouble);
#ifdef __cplusplus
}
#endif
#endif
C实现文件pers_zhang_jni_Printf2.c:
#include "pers_zhang_jni_Printf2.h"
#include <string.h>
#include <stdlib.h>
#include <float.h>
char* find_format(const char format[])
{
char* p;
char* q;
p = strchr(format, '%');
while(p != NULL && *(p + 1) == '%')
p = strchr(p + 2, '%');
if(p == NULL)
return NULL;
p++;
q = strchr(p, '%');
while(q != NULL && *(q + 1) == '%')
q = strchr(q + 2, '%');
if(q != NULL)
return NULL;
q = p + strspn(p, " -0+#");
q += strspn(q, "0123456789");
if(*q == '.')
{
q++;
q += strspn(q, "0123456789");
}
if(strchr("eEfFgG", *q) == NULL)
return NULL;
return p;
}
JNIEXPORT jstring JNICALL Java_pers_zhang_jni_Printf2_sprint(JNIEnv * env, jclass cl, jstring format, jdouble x)
{
const char* cformat;
char* fmt;
jstring ret;
cformat = (*env)->GetStringUTFChars(env, format, NULL);
fmt = find_format(cformat);
if(fmt == NULL)
ret = format;
else
{
char* cret;
int width = atoi(fmt);
if(width == 0)
width = DBL_DIG + 10;
cret = (char*)malloc(strlen(cformat) + width);
sprintf(cret, cformat, x);
ret = (*env)->NewStringUTF(env, cret);
free(cret);
}
(*env)->ReleaseStringUTFChars(env, format, cformat);
return ret;
}
在本函数中,我们选择简化错误处理。如果打印浮点数的格式代码不是%w.pc
形式的(其中c是e、E、f、g或G中的一个),那么我们将不对数字进行格式化。
编译为动态库Printf2.dylib:
gcc -I $JAVA_HOME/include -I $JAVA_HOME/include/darwin -dynamiclib -o Printf2.dylib pers_zhang_jni_Printf2.c
测试文件Printf2Test.java:
public class Printf2Test {
static {
System.load("/Users/acton_zhang/J2EE/MavenWorkSpace/java_safe_demo/src/main/java/pers/zhang/jni/Printf2.dylib");
}
public static void main(String[] args) {
double price = 44.95;
double tax = 7.75;
double amountDue = price * (1 + tax / 100);
String s = Printf2.sprint("Amount due = %8.2f", amountDue);
System.out.println(s);
}
}
输出:
Amount due = 48.43
4、访问域
4.1、访问实例域
案例为调用实例方法,Employee.java:
package pers.zhang.jni;
public class Employee {
private String name;
private double salary;
public Employee(String name, double salary) {
this.name = name;
this.salary = salary;
}
//本地方法
public native void raiseSalary(double byPercent);
public void print() {
System.out.println(name + " " + salary);
}
}
生成头文件pers_zhang_jni_Employee.h:
javac -h . Employee.java
/* DO NOT EDIT THIS FILE - it is machine generated */
#include <jni.h>
/* Header for class pers_zhang_jni_Employee */
#ifndef _Included_pers_zhang_jni_Employee
#define _Included_pers_zhang_jni_Employee
#ifdef __cplusplus
extern "C" {
#endif
/*
* Class: pers_zhang_jni_Employee
* Method: raiseSalary
* Signature: (D)V
*/
JNIEXPORT void JNICALL Java_pers_zhang_jni_Employee_raiseSalary
(JNIEnv *, jobject, jdouble);
#ifdef __cplusplus
}
#endif
#endif
注意,方法的第二个参数不再是jclass
类型而是jobject
类型。实际上,它和this引用
等价。静态方法得到的是类的引用
,而非静态方法得到的是对隐式的this参数对象的引用。
现在,我们访问隐式参数的salary域。在Javal.0中“原生的”Java到C的绑定中,这很简单,程序员可以直接访问对象数据域。然而,直接访问要求虚拟机暴露它们的内部数据布局。基于这个原因,JNI要求程序员通过调用特殊的JNI函数来获取和设置数据的值。
在例子里,要使用GetdoubleField
和SetDoubleField
函数,因为salary是double类型的。对于其他类型,可以使用的函数有:GetIntField/SetIntField
、GetObjectField/SetObjectField
等等。其通用语法是:
x = (*env)->GetXxxField(env, this_obj, fieldID);
(*env)->SetXxxField(env, this_obj, fieldID, x);
这里,fieldID
是一个特殊类型jfieldID的值,jfieldID标识结构中的一个域,而Xxx
代表Java数据类型(Object、Boolean、Byte或其他)。为了获得fieldID,必须先获得一个表示类的值,有两种方法可以实现此目的。GetObjectClass
函数可以返回任意对象的类。例如:
jclass class_Employee = (*env)->GetObjectClass(env, this_obj);
FindClass
函数可以以字符串形式来指定类名(要以/
代替句号作为包名之间的分隔符)。
jclass class_String = (*env)->FindClass(env, "java/lang/String");
之后,可以使用GetFieldID
函数来获得fieldID。必须提供域的名字、它的签名以及它的类型的编码。例如,下面是从salary域得到fieldID的代码:
jfieldID id_salary = (*env)->GetFieldID(env, class_Employee, "salary", "D");
C实现文件pers_zhang_jni_Employee.c:
#include "pers_zhang_jni_Employee.h"
JNIEXPORT void JNICALL Java_pers_zhang_jni_Employee_raiseSalary(JNIEnv * env, jobject this_obj, jdouble byPercent)
{
//获得类
jclass class_Employee = (*env)->GetObjectClass(env, this_obj);
//获得fieldID
jfieldID id_salary = (*env)->GetFieldID(env, class_Employee, "salary", "D");
//获得字段值
jdouble salary = (*env)->GetDoubleField(env, this_obj, id_salary);
//计算
salary *= 1 + byPercent / 100;
//设置到字段
(*env)->SetDoubleField(env, this_obj, id_salary, salary);
}
警告:类引用只在本地方法返回之前有效。因此,不能在代码中缓存GetObjectClss的返回值。不要将类引用保存下来以供以后的方法调用重复使用。必须在每次执行本地方法时都调用GetObjectClass。如果无法忍受这一点,必须调用NewGlobalRef来锁定该引用:
static jclass class_X = 0;
static jfieldID id_a;
...
if(class_X == 0)
{
jclass cx = (*env)->GetObjectClass(env, obj);
class_X = (*env)->NewGlobalRef(env, cx);
id_a = (*env)->GetFieldID(env, cls, "a","...");
}
//现在,嗯可以在后面的调用中使用类引用和域ID了,当结束对类的使用时,务必调用:
(*env)->DeleteGlobalRef(env, class_X);
编译为动态库Employee.dylib:
gcc -I $JAVA_HOME/include -I $JAVA_HOME/include/darwin -dynamiclib -o Employee.dylib pers_zhang_jni_Employee.c
测试文件EmployeeTest.java:
public class EmployeeTest {
static {
System.load("/Users/acton_zhang/J2EE/MavenWorkSpace/java_safe_demo/src/main/java/pers/zhang/jni/Employee.dylib");
}
public static void main(String[] args) {
Employee[] staff = new Employee[3];
staff[0] = new Employee("Harry Hacker", 35000);
staff[1] = new Employee("Carl Cracker", 75000);
staff[2] = new Employee("Tony Tester", 38000);
for (Employee e : staff) {
e.raiseSalary(5);
}
for (Employee e : staff) {
e.print();
}
}
}
输出:
Harry Hacker 36750.0
Carl Cracker 78750.0
Tony Tester 39900.0
4.2、访问静态域
访问静态域和访问非静态域类似,要使用GetStaticFieldID
和GetStaticXxxField/SetStaticXxxField
函数。它们几乎与非静态的情形一样,只有两个区别:
- 由于没有对象,所以必须使用FindClass代替GetObjectClass来获得类引用。
- 访问域时,要提供类而非实例对象。
示例:得到System.out的引用
//获得类
jclass class_System = (*env)->FindClass(env, "java/lang/System");
//获得fieldID
jfieldID id_out = (*env)->GetStaticFieldID(env, class_Sytem, "out", "Ljava/io/PrintStream;");
//获得引用
jobject obj_out = (*env)->GetStaticObjectField(env, class_System, id_out);
4.3、常用方法
- jfieldID GetFieldID(JNIEnv *env, jclass cl, const char name[], const char fieldSignature[]):返回类中的一个域的标识符。
- Xxx GetXxxField(JNIEnv *env, jobject obj, jfieldID id):返回域的值。域类型Xxx是Object、Boolean、Byte、Char、Short、Int、Long、Float或Double之一。
- void SetXxxField(JNIEnv *env, jobject obj, jfieldID id, Xxx value):把某个域设置为一个新值。域类型Xxx是Object、Boolean、Byte、Char、Short、Int、Long、Float或Double之一。
- jfieldID GetStaticFieldID(JNIEnv *env, jclass cl, const char name[], const char fieldSignature[]):返回某类型的一个静态域的标识符。
- Xxx GetStaticXxxField(JNIEnv *env, jclass cl, jfieldID id):返回某静态域的值。域类型Xxx是Object、Boolean、Byte、Char、Short、Int、Long、Float或Double之一。
- void SetStaticXxxField(JNIEnv *env, jclass cl, jfieldID id, Xxx value):把某个静态域设置为一个新值。域类型Xxx是Object、Boolean、Byte、Char、Short、Int、Long、Float或Double之一。
5、编码签名
数据类型描述符如下表:
类型 | 描述符 |
---|---|
byte | B |
char | C |
double | D |
float | F |
int | I |
long | J |
short | S |
boolean | Z |
void | V |
类 | L+classname+; |
数组 | [ |
方法签名 | (args)return |
要建立一个方法的完整签名,需要把括号内的参数类型都列出来,然后列出返回值类型。例如,一个接收两个整形参数,一个整形数组参数并返回一个整数的方法签名为:
(II[I)I
Employee类的构造器签名为:
(Ljava/lang/String;D)V
提示:可以使用带有选项-s的javap命令来从类文件中产生方法签名。
例如:
javap -s -private Employee
可以得到以下显示所有域和方法的描述/签名:
Compiled from "Employee.java"
public class pers.zhang.jni.Employee {
private java.lang.String name;
descriptor: Ljava/lang/String;
private double salary;
descriptor: D
public pers.zhang.jni.Employee(java.lang.String, double);
descriptor: (Ljava/lang/String;D)V
public native void raiseSalary(double);
descriptor: (D)V
public void print();
descriptor: ()V
}
6、C调用Java方法
6.1、实例方法
示例:使用装饰器模式增强Printf类,给它增加一个与C函数fprintf类似的方法。也就是说,它能够在任意PrintWriter对象上打印一个字符串。
package pers.zhang.jni;
import java.io.PrintWriter;
public class Printf3 {
public static native void fprint(PrintWriter out, String format, double x);
}
生成头文件pers_zhang_jni_Printf3.h:
javac -h . Printf3.java
/* DO NOT EDIT THIS FILE - it is machine generated */
#include <jni.h>
/* Header for class pers_zhang_jni_Printf3 */
#ifndef _Included_pers_zhang_jni_Printf3
#define _Included_pers_zhang_jni_Printf3
#ifdef __cplusplus
extern "C" {
#endif
/*
* Class: pers_zhang_jni_Printf3
* Method: fprint
* Signature: (Ljava/io/PrintWriter;Ljava/lang/String;D)V
*/
JNIEXPORT void JNICALL Java_pers_zhang_jni_Printf3_fprint
(JNIEnv *, jclass, jobject, jstring, jdouble);
#ifdef __cplusplus
}
#endif
#endif
C实现文件pers_zhang_jni_Printf3:
首先要把打印的字符串组装成一个String对象str,然后从实现本地方法的C函数中调用PrintWriter类的print方法。
使用如下函数掉哟个,可以从C中调用任何Java方法:
(*env)->CallXxxMethod(env, implicit parameter, methodID, explicit parameters)
根据方法的返回类型,用Void、Int、Object等来替换Xxx。就像需要一个fieldID来访问某个对象的一个域一样,还需要一个方法的ID来调用方法。可以通过调用JNI函数GetMethodID
,并且提供该类、方法的名字和方法签名来获得方法ID。
在我们的例子中,我们想要获得Printwriter类的print方法的ID。PrintWriter类有几个名为pit的重载方法。基于这个原因,还必须提供一个方法签名,描述想要使用的特定函数的参数和返回值。例如,我们想要使用void print(java.lang.String)方法,对应的签名为(Ljava//Lang/String;)V"
。
完整的实现代码:
#include "pers_zhang_jni_Printf3.h"
#include <string.h>
#include <stdlib.h>
#include <float.h>
char* find_format(const char format[])
{
char* p;
char* q;
p = strchr(format, '%');
while(p != NULL && *(p + 1) == '%')
p = strchr(p + 2, '%');
if(p == NULL)
return NULL;
p++;
q = strchr(p, '%');
while(q != NULL && *(q + 1) == '%')
q = strchr(q + 2, '%');
if(q != NULL)
return NULL;
q = p + strspn(p, " -0+#");
q += strspn(q, "0123456789");
if(*q == '.')
{
q++;
q += strspn(q, "0123456789");
}
if(strchr("eEfFgG", *q) == NULL)
return NULL;
return p;
}
JNIEXPORT void JNICALL Java_pers_zhang_jni_Printf3_fprint(JNIEnv * env, jclass cl, jobject out, jstring format, jdouble x)
{
const char* cformat;
char* fmt;
jstring str;
jclass class_PrintWriter;
jmethodID id_print;
cformat = (*env)->GetStringUTFChars(env, format, NULL);
fmt = find_format(cformat);
if(fmt == NULL)
str = format;
else
{
char* cstr;
int width = atoi(fmt);
if(width == 0)
width = DBL_DIG + 10;
cstr = (char*)malloc(strlen(cformat) + width);
sprintf(cstr, cformat, x);
str = (*env)->NewStringUTF(env, cstr);
free(cstr);
}
//获取类
class_PrintWriter = (*env)->GetObjectClass(env, out);
//获得methodID
id_print = (*env)->GetMethodID(env, class_PrintWriter, "print", "(Ljava/lang/String;)V");
//调用方法
(*env)->CallVoidMethod(env, out, id_print, str);
}
数值型的方法ID和域ID在概念上和反射API中的Method和Field对象类似。可以使用以下函数在两者间进行转换:
jobject ToRelectedMethod(JNIEnv* env, jclass class, jmethodID methodID);
methodID FromReflectedMethod(JNIEnv* env, jobject method);
jobject ToReflectedField(JNIEnv* env, jclass class, jfieldID fieldID);
fieldID FromReflectedField(JNIEnv* env, jobject field);
编译为动态库Printf3.dylib:
gcc -I $JAVA_HOME/include -I $JAVA_HOME/include/darwin -dynamiclib -o Printf3.dylib pers_zhang_jni_Printf3.c
测试文件Printf3Test.java:
public class Printf3Test {
static {
System.load("/Users/acton_zhang/J2EE/MavenWorkSpace/java_safe_demo/src/main/java/pers/zhang/jni/Printf3.dylib");
}
public static void main(String[] args) {
double price = 44.95;
double tax = 7.75;
double amountDue = price * (1 + tax / 100);
PrintWriter out = new PrintWriter(System.out);
Printf3.fprint(out, "Amount due = %8.2f\n", amountDue);
out.flush();
}
}
输出:
Amount due = 48.43
6.2、静态方法
从本地方法调用静态方法与调用非静态方法类似。两者的差别是:
- 要用
GetStaticMethodID
和CallStaticXxxMethod
函数。 - 当调用方法时,要提供类对象,而不是隐式的参数对象。
示例,从C代码中调用以下静态方法:
System.getProperty("java.class.path");
//首先找到要用的类,因为没有System类的对象可供使用,所以使用FindClass而非GetObjectClass
jclass class_System = (*env)->FindClass(env, "java/lang/System");
//获得静态getProperty方法的ID
jmethodID id_getProeprty = (*env)->GetStaticMethodID(env, class_Sstem, "getProperty", "(Ljava/lang/String;)Ljava/lang/String;");
//调用
jobject obj_ret = (*env)->CallStaticObjectMethod(env, class_System, id_getProperty, (*env)->NewStringUTF(env, "java.class.path"));
//返回值是jobject类型的,如果想要把它当做字符串操作,必须把它转型为jstring
jstring str_ret = (jstring)obj_ret;
C++注释:在C中,jstring和jclass类型和数组类型一样,都是与jObject等价的类型。因此,在C语言中,前面例子中的转型并不是严格必需的。但是在C++中,这些类型被定义为指向拥有正确继承层次关系的“哑类”的指针。例
如,将一个jstring不经过转型便赋给jobject在C++中是合法的,但是将jobject赋给jstring必须先转型。
6.3、构造器
本地方法可以通过调用构造器来创建新的Java对象。可以调用NewObject函数来调用构造器。
jobject obj_new = (*env)->NewObject(env, class, methodID, construction parameters);1
可以通过指定方法名为"<init>
",并指定构造器(返回值为void)的编码签名,从GetMethodID函数中获取该调用必需的方法ID。
例如,下面是本地方法创建FileOutputStream对象的情形:
const char[] fileName = "...";
jstring str_fileName = (*env)->NewStringUTF(env, fileName);
jclass class_FileOutputStream = (*env)->FindClass(env, "java/io/FileOutputStram");
jmethodID id_FileOutputStream = (*env)->GetMethodID(env, class_FileOutputStream, "<init>", "(Ljava/lang/String;)V");
jobject obj_stream = (*env)->NewObject(env, class_FileOutputStream, id_FileOutputStream, str_fileName);
6.4、另一种方法调用
有若干种JNI函数的变体都可以从本地代码调用Java方法。它们没有我们已经讨论过的那些函数那么重要,但偶尔也会很有用。
CallNonvirtualXxxMethod
函数接收一个隐式参数、一个方法ID、一个类对象(必须对应于隐式参数的超类)和一个显式参数。这个函数将调用指定的类中的指定版本的方法,而不使用常规的动态调度机制。
所有调用函数都有后缀"A"和"V"的版本,用于接收数组中或va_list中的显式参数(就像在C头文件stdarg.h中所定义的那样)。
6.5、常用方法
- jmethodID GetMethodID(JNIEnv *env, jclass cl, const char name[], const char methodSignature[]):返回某类中某个方法的标识符。
- Xxx CallXxxMethod(JNIEnv *env, jobect obj, jmethoID id, args)
- Xxx CallXxxMethodA(JNIEnv *env, jobect obj, jmethoID id, jvalue args[])
- Xxx CallXxxMethodV(JNIEnv *env, jobect obj, jmethoID id, va_list args):调用一个方法。返回类型Xxx是Object、Boolean、Byte、Char、Short、Int、LongFloat或Double之一。第一个函数有可变数量参数,只要把方法参数附加到方法ID之后即可。第二个函数接受jvalue数组中的方法参数。第三个函数接收C头文件stdarg.h中定义的va_list中的方法参数。
javalue是一个联合体,定义如下:
typedef union jvalue
{
jboolean z;
jbyte b;
jchar c;
jshort s;
jint i;
jlong j;
jfloat f;
jdouble d;
jobject l;
} javlue;
- Xxx CallNonvirtualXxxMethod(JNIEnv *env, jobject obj, jclass cl, jmethodID id, args)
- Xxx CallNonvirtualXxxMethod(JNIEnv *env, jobject obj, jclass cl, jmethodID id, javlue args[])
- Xxx CallNonvirtualXxxMethod(JNIEnv *env, jobject obj, jclass cl, jmethodID id, va_list args):调用一个方法,并绕过动态调度。返回类型Xxx是Object、Boolean、Byte、Char、Short、Int、LongFloat或Double之一。第一个函数有可变数量参数,只要把方法参数附加到方法ID之后即可。第二个函数接受jvalue数组中的方法参数。第三个函数接收C头文件stdarg.h中定义的va_list中的方法参数。
- jmethodID GetStaticMethodID(JNIEnv *env, jclass cl, const char name[], const char methodSignatrue[]):返回类的某个静态方法的标识符。
- Xxx CallStaticXxxMethod(JNIEnv *env, jclass cl, jmethodID id, args)
- Xxx CallStaticXxxMethod(JNIEnv *env, jclass cl, jmethodID id, jvalue args[])
- Xxx CallStaticXxxMethod(JNIEnv *env, jclass cl, jmethodID id, va_list args):调用一个静态方法。返回类型Xxx是Object、Boolean、Byte、Char、Short、Int、LongFloat或Double之一。第一个函数有可变数量参数,只要把方法参数附加到方法ID之后即可。第二个函数接受jvalue数组中的方法参数。第三个函数接收C头文件stdarg.h中定义的va_list中的方法参数。
- jobject NewObject(JNIEnv *env, jclass cl, jmethodID id, args)
- jobject NewObject(JNIEnv *env, jclass cl, jmethodID id, jvalue args[])
- jobject NewObject(JNIEnv *env, jclass cl, jmethodID id, va_list args):调用构造器。函数ID从钓鱼函数名为
<init>
和返回类型为void的GetMethodID
获取。第一个函数有可变数量参数,只要把方法参数附加到方法ID之后即可。第二个函数接受jvalue数组中的方法参数。第三个函数接收C头文件stdarg.h中定义的va_list中的方法参数。
7、访问数组元素
7.1、Java数组类型和C数组类型之间的对应关系
Java数组类型 | C数组类型 |
---|---|
boolean[] | jbooleanArray |
byte[] | jbyteArray |
char[] | jcharArray |
int[] | jintArray |
short[] | jshortArray |
long[] | jlongArray |
float[] | jfloatArray |
double[] | jdoubleArray |
Object[] | jobjectArray |
C++:在C中,所有这些数组类型实际上都是jobject的同义类型。然而,在C++中它们被安排在如图下所示的继承层次结构中。jarray类型表示一个泛型数组。
7.2、常用方法
- jsize GetArrayLength(JNIEnv *env, jarray array):返回数组中的元素个数。
- jobject GetObjectArrayElement(JNIEnv *env, jobjectArray array, jsize index):返回数组元素的值。
- void SetObjectArrayElement(JNIEnv *env, jobjectArray array, jsize index, jobject value):将数组元素设为新值。
- Xxx* GetXxxArrayElements(JNIEnv env, jarray array, jboolean isCopy):产生一个指向JAVA数组元素的C指针。域类型Xxx是Boolean、Byte、Char、Short、It、Long、Float或Double之一。指针不再使用时,该指针必须传递给
ReleaseXxxArrayElements
。iscopy可能是NULL,或者在进行复制时,指向用JNI_TRUE填充的jboolean;否则,指向用JNI_FALSE填充的jboolean。 - void ReleaseXxxArrayElements(JNIEnv *env, jarray array, Xxx elems[], jint mode):通知虚拟机通过GetXxxArrayElements获得的一个指针已经不再需要了。Mode是0(更新数组元素后释放elems缓存)、JNI_COMMIT(更新数组元素后不释放elems缓存)或JNI_ABORT(不更新数组元素便释放elems缓存)之一。
- void GetXxxArrayRegion(JNIEnv *env, jarray array, jint start, jint length, Xxx elems[]):将Java数组的元素复制到C数组中。域类型Xxx是Boolean、Byte、Char、Short、It、Long、Float或Double之一。
- void SetXxxArrayRegion(JNIEnv *env, jarray array, jint length, Xxx elems[]);将C数组的元素复制到Java数组找那个。域类型Xxx是Boolean、Byte、Char、Short、It、Long、Float或Double之一。
7.3、示例
C语言实现逆序数组。
package pers.zhang.jni;
public class ArrayTest {
static {
System.load("/Users/acton_zhang/J2EE/MavenWorkSpace/java_safe_demo/src/main/java/pers/zhang/jni/ArrayTest.dylib");
}
public static void main(String[] args) {
double[] arr = new double[]{1.0, 2.0, 3.0, 4.0, 5.0};
reverse(arr);
for (double num : arr) {
System.out.print(num + "\t");
}
}
public static native void reverse(double[] arr);
}
生成头文件pers_zhang_jni_ArrayTest.h:
javac -h . ArrayTest.java
/* DO NOT EDIT THIS FILE - it is machine generated */
#include <jni.h>
/* Header for class pers_zhang_jni_ArrayTest */
#ifndef _Included_pers_zhang_jni_ArrayTest
#define _Included_pers_zhang_jni_ArrayTest
#ifdef __cplusplus
extern "C" {
#endif
/*
* Class: pers_zhang_jni_ArrayTest
* Method: reverse
* Signature: ([D)V
*/
JNIEXPORT void JNICALL Java_pers_zhang_jni_ArrayTest_reverse
(JNIEnv *, jclass, jdoubleArray);
#ifdef __cplusplus
}
#endif
#endif
实现文件pers_zhang_jni_ArrayTest.c:
#include "pers_zhang_jni_ArrayTest.h"
JNIEXPORT void JNICALL Java_pers_zhang_jni_ArrayTest_reverse(JNIEnv * env, jclass cl, jdoubleArray double_array)
{
//获取数组长度
jsize length = (*env)->GetArrayLength(env, double_array);
//获取指向java数组的指针
double* arr = (*env)->GetDoubleArrayElements(env, double_array, NULL);
//逆序
for (int i = 0; i < (length - 1) / 2; i++)
{
int temp = arr[i];
arr[i] = arr[length - 1 - i];
arr[length - 1 - i] = temp;
}
//更新数组之后再释放指针
(*env)->ReleaseDoubleArrayElements(env, double_array, arr, 0);
}
编译为动态库ArrayTest.dylib:
gcc -I $JAVA_HOME/include -I $JAVA_HOME/include/darwin -dynamiclib -o ArrayTest.dylib pers_zhang_jni_ArrayTest.c
测试后输出:
6.0 5.0 3.0 4.0 2.0 1.0
8、错误处理
8.1、概述
在Java编程语言中,使用本地方法对于程序来说是要冒很大的安全风险的。C的运行期系统对数组越界错误、不良指针造成的间接错误等不提供任何防护。所以,对于本地方法的程序员来说,处理所有的出错条件以保持Java平台的完整性显得格外重要。尤其是,当本地方法诊断出一个它无法解决的问题时,那么它应该将此问题报告给Jva虚拟机。然后,在这种情况下,很自然地会抛出一个异常。然而,C语言没有异常,必须调用Throw
或ThrowNew
函数来创建一个新的异常对象。当本地方法退出时,Java虚拟机就会抛出该异常。
要使用Throw函数,需要调用NewObject来创建一个Throwable子类的对象。例如,下面分配一个EOFException对象,然后将它抛出:
jclass class_EOFException = (*env)->FindClass(env, "java/io/EOFException");
jmethodID id_EOFException = (*env)->GetMethodID(env, class_EOFException, "<init>", "()V");
jthrowable obj_exc = (*env)->NewObject(env, class_EOFException, id_EOFException);
(*env)->Throw(env, obj_exc);
通常会调用ThrowNew会更加方便,因为只需要提供一个类和一个"modified UTF-8"字节序列,该函数就会构建一耳光异常对象:
(*env)->ThrowNew(env, (*env)->FindClass(env, "java/io/EOFExceptino"), "Unexpected end of file");
Throw和ThrowNew都只是发布异常,它们不会中断本地方法的控制流。只有当该方法返回时,Java虚拟机才会抛出异常。所以,每一个对Throw和ThrowNew的调用语句之后总是紧跟着return语句。
通常,本地代码不需要考虑捕获Java异常。但是,当本地方法调用Java方法时,该方法可能会抛出异常。而且,一些JNI函数也会抛出异常。
例如,如果索引越界,SetObjectArrayElement方法会抛出一个ArrayIndexOut0fBoundsException异常,如果所存储的对象的类不是数组元素类的子类,该方法会抛出一个ArrayStoreException异常。在这类情况下,本地方法应
该调用·ExceptionOccurred·方法来确认是否有异常抛出。如果没有任何异常等待处理,则下面的调用:
jthrowable obj_exc = (*env)->ExceptionOccurred(env);
将返回NULL。否则,返回一个当前异常对象的引用。如果只要检查是否有异常抛出,而不需要获得异常对象的引用,那么应使用:
jboolean occurred = (*env)->ExceptionCheck(env);
通常,有异常出现时,本地方法应该直接返回。那样,虚拟机就会将该异常传送给Java代码。但是,本地方法也可以分析异常对象,确定它是否能够处理该异常。如果能够处理,那么必须调用下面的函数来关闭该异常:
(*env)->ExceptionClear(env);
8.2、常用方法
- jint Throw(JNIEnv *env, jthrowable obj): 准备一个在本地代码退出时抛出的异常。成功时返回0,失败时返回一个负值。
- jint ThrowNew(JNIEnv *env, jclass cl, const char msg[]): 准备一个在本地代码退出时抛出的类型为cl的异常。成功时返回0,失败时返回一个负值。msg是表示异常对象的String构造参数的"modified UTF-8"字节序列。
- jthrowable ExcepitonOccurred(JNIEnv *env): 如果有异常挂起,则返回该异常对象,否则返回NULL。
- jboolean ExceptionCheck(JNIEnv *env): 如果有异常挂起,则返回true。
- void ExceptionClear(JNIenv *env): 清除挂起的异常。
8.3、示例
在例子中,实现了fprint本地方法,这是基于该方法适合编写为本地方法的假设而实现的。下面是我们抛出的异常:
- 如果格式字符串是NULL,则抛出
NullPointerException
异常。 - 如果格式字符串不含适合打印double所需的%说明符,则抛出
IllegalArgumentException
异常。 - 如果调用malloc失败,则抛出
OutOfMemoryError
异常。
最后,为了说明本地方法调用Java方法时,怎样检查异常,我们将一个字符串发送给数据流,一次一个字符,并且在每次调用Java方法后调用ExceptionOccurred。注意,在调用Printwriter.print出现异常时,本地方法并不会立即终止执行,它会首先释放cstr缓存。当本地方法返回时,虚拟机再次抛出异常。
Printf4.Java文件:
package pers.zhang.jni;
import java.io.PrintWriter;
public class Printf4 {
public static native void fprint(PrintWriter ps, String format, double x);
}
生成头文件pers_zhang_jni_Printf4.h:
javac -h . Printf4.java
/* DO NOT EDIT THIS FILE - it is machine generated */
#include <jni.h>
/* Header for class pers_zhang_jni_Printf4 */
#ifndef _Included_pers_zhang_jni_Printf4
#define _Included_pers_zhang_jni_Printf4
#ifdef __cplusplus
extern "C" {
#endif
/*
* Class: pers_zhang_jni_Printf4
* Method: fprint
* Signature: (Ljava/io/PrintWriter;Ljava/lang/String;D)V
*/
JNIEXPORT void JNICALL Java_pers_zhang_jni_Printf4_fprint
(JNIEnv *, jclass, jobject, jstring, jdouble);
#ifdef __cplusplus
}
#endif
#endif
C实现文件pers_zhang_jni_Printf4.c:
#include "pers_zhang_jni_Printf4.h"
#include <string.h>
#include <stdlib.h>
#include <float.h>
char* find_format(const char format[])
{
char* p;
char* q;
p = strchr(format, '%');
while(p != NULL && *(p + 1) == '%')
p = strchr(p + 2, '%');
if(p == NULL)
return NULL;
p++;
q = strchr(p, '%');
while(q != NULL && *(q + 1) == '%')
q = strchr(q + 2, '%');
if(q != NULL)
return NULL;
q = p + strspn(p, " -0+#");
q += strspn(q, "0123456789");
if(*q == '.')
{
q++;
q += strspn(q, "0123456789");
}
if(strchr("eEfFgG", *q) == NULL)
return NULL;
return p;
}
JNIEXPORT void JNICALL Java_pers_zhang_jni_Printf4_fprint(JNIEnv * env, jclass cl, jobject out, jstring format, jdouble x)
{
const char* cformat;
char* fmt;
jclass class_PrintWriter;
jmethodID id_print;
char* cstr;
int width;
int i;
//输入的参数为空,抛出异常
if(format == NULL)
{
(*env)->ThrowNew(env, (*env)->FindClass(env, "java/lang/NullPointerException"), "Printf4.fprint:format is null");
return;
}
cformat = (*env)->GetStringUTFChars(env, format, NULL);
fmt = find_format(cformat);
//格式化字符串为不符合标准,抛出异常
if(fmt == NULL)
{
(*env)->ThrowNew(env, (*env)->FindClass(env, "java/lang/IllegalArgumentException"), "Printf4.fprint:format is invalid");
return;
}
width = atoi(fmt);
if(width == 0)
width = DBL_DIG + 10;
cstr = (char*)malloc(strlen(cformat) + width);
if(cstr == NULL)
{
(*env)->ThrowNew(env, (*env)->FindClass(env, "java/lang/OutOfMemoryError"), "Printf4.fprint:malloc failed");
return;
}
sprintf(cstr, cformat, x);
(*env)->ReleaseStringUTFChars(env, format, cformat);
class_PrintWriter = (*env)->GetObjectClass(env, out);
id_print = (*env)->GetMethodID(env, class_PrintWriter, "print", "(C)V");
for(i = 0; cstr[i] != 0 && !(*env)->ExceptionOccurred(env); i++)
{
(*env)->CallVoidMethod(env, out, id_print, cstr[i]);
}
free(cstr);
}
编译为动态库Printf4.dylib:
gcc -I $JAVA_HOME/include -I $JAVA_HOME/include/darwin -dynamiclib -o Printf4.dylib pers_zhang_jni_Printf4.c
测试文件Printf4Test.java:
public class Printf4Test {
static {
System.load("/Users/acton_zhang/J2EE/MavenWorkSpace/java_safe_demo/src/main/java/pers/zhang/jni/Printf4.dylib");
}
public static void main(String[] args) {
double price = 44.95;
double tax = 7.75;
double amountDue = price * (1 + tax / 100);
PrintWriter out = new PrintWriter(System.out);
//%%8.2f 格式不正确,抛出异常
Printf4.fprint(out,"Amount due = %%8.2f\n", amountDue);
out.flush();
}
}
输出:
Exception in thread "main" java.lang.IllegalArgumentException: Printf4.fprint:format is invalid
at pers.zhang.jni.Printf4.fprint(Native Method)
at pers.zhang.jni.Printf4Test.main(Printf4Test.java:22)
9、使用调用API(invocation API)
之前都是Java程序调用C代码,假设在相反的情况下,有一个C或者C++的程序,并且想要调用一些Java代码。调用API(invocation API)能够把Java虚拟机嵌入到C或者C++程序中。
下面是初始化虚拟机所需的基本代码:
JavaVMOption options[1];
JavaVMInitArgs vm_ags;
JavaVM *jvm;
JNIEnv *env;
options[0].optionString = "-Djava.class.path=.";
memset(&vm_args, 0, sizeof(vm_args));
vm_args.version = JNI_VERSION_1_2;
vm_args.nOptions = 1;
vm_args.options = options;
JNI_CreateJavaVM(&jvm, (void**)&env, &vm_args);
对JNI_CreateJavaVM
的调用将创建虚拟机,并且使指针jvm
指向虚拟机,使指针env
指向执行环境。
可以给虚拟机提供任意数目的选项,这只需增加选项数组的大小和vm args.nOptions的值。例如,
options[i].optionString = "-Djava.compiler=NONE";
可以钝化即时编译器。
一旦设置完虚拟机,就可以如前面的那样调用Java方法了。只要按常规方法使用env指针即可。
只有在调用API中的其他函数时,才需要jm指针。目前,只有四个这样的函数。最重要的一个是终止虚拟机的函数:
(*jvm)->DestroyJavaVM(jvm);