您的位置:首页 > 编程语言 > Java开发

JNI 学习手记

2013-11-28 11:26 309 查看

-——————12月27号追加说明——————

关于javah的用法:

jdk6和jdk7的javah的用法是不一样的。对于jdk6,也就是我下文的主要环境,是对.class文件进行操作的:javah MyClass.class但是jdk7是针对命名空间进行操作的:
javah MyClass
也就是说,当 .java文件是在一个包里的,如下:
package MyPackage;

public class MyCode {
public static native void myNative();
public static void main (String[] args) {
myNative();
}
}
jdk6的javah和一般的并没有区别,而jdk7则要知道命名空间后才能用:
javah MyPackage.MyCode从语义来讲,jdk7的更加统一了,然而从某种程度上破坏了用jdk6开发的代码的一致性

———————原文————————

近来要做java的一个项目,必须和外部设备通信,因此内核必须是用C来完成。然而C写交互界面过于麻烦,因此上层必须用Java实现,最后考虑下来的结果,就是用JNI来调用C的接口,让工作可以尽量迅速而简单地完成。工作平台是Debian,详细的系统和硬件信息在最后,本文结合完全本人编写的代码进行讨论。

如果大家看了有什么想法,或者有什么问题,希望提出来探讨探讨。

JNI手册地址:JNI Document

一  Hello World

这一节是牛刀小试,主要是先尝试在JVM上run C code,先编写java代码:

public class HelloWorld {
public native void showHelloWorld();
static {
System.loadLibrary("hello");
}

public static void main(String[] args) {
new HelloWorld().showHelloWorld();
}
}

然后编译:

hu@forhu-debian:~/Java code/JNI code/Helloworld$ javac HelloWorld.java


这时候生成了.class文件,再用javah生成头文件:

hu@forhu-debian:~/Java code/JNI code/Helloworld$ javah HelloWorld.class


一般而言,不报错就是成功生成。下一步是根据头文件编写C文件:

#include <jni.h>
#include "HelloWorld.h"
#include <stdio.h>

JNIEXPORT void JNICALL Java_HelloWorld_showHelloWorld (JNIEnv *env, jobject obj){
printf("Hello world!\n");
return;
}
然后再只要生成动态链接库就可以完成工作:

hu@forhu-debian:~/Java code/JNI code/Helloworld$ gcc -std=c99 -shared -o ~/lib/libhello.so HelloWorld.c
再run java程序就能看到结果:

hu@forhu-debian:~/Java code/JNI code/Helloworld$ java HelloWorld
Hello world!
这时候要注意是否把.so文件生成到了动态链接的路径上,如果没有,可以手动添加这个路径到LD_LIBRARY_PATH环境变量:

export LD_LIBRARY_PATH=$HOME/lib


二  解析头文件

看JNI的doc里的resolving native method names一节里面解释了C里面的name mangling的规范,这个其实和C++的规范差不多,学过点C++的应该就不会觉得陌生,先看头文件 :

/* DO NOT EDIT THIS FILE - it is machine generated */

#include <jni.h>

#ifndef __HelloWorld__
#define __HelloWorld__

#ifdef __cplusplus
extern "C"
{
#endif

JNIEXPORT void JNICALL Java_HelloWorld_showHelloWorld (JNIEnv *env, jobject);

#ifdef __cplusplus
}
#endif

#endif /* __HelloWorld__ */


doc里的描述如下:

Dynamic linkers resolve entries based on their names. A native method name is concatenated from the following components:

the prefix
Java_


a mangled fully-qualified class name

an underscore (“_”) separator

a mangled method name

for overloaded native methods, two underscores (“__”) followed by the mangled argument signature

分别解释一下就是:

一个比如那存在的Java_ 前缀
然后接着是类的全名 (包括包)
然后是下划线_
然后是一个mangle过的方法名
JNI中mangle的意思就是当函数重载的时候加上__后在标上传入参数的签名,这个特性在这里没有体现
具体的签名规范看doc的第三章有讲述:Type Signatures

在知道这些命名规范的前提下,我们就可以知道在C下实现的函数对应于Java中的哪个方法了

三  返回数组

这一节尝试返回一个double数组,其他数组应该是同理:

public class GDA {
protected static native double[] genDoubleArray(int length);

static {
System.loadLibrary("GenDoubleArr");
}

public static void main (String[] args){
int limit=10;
double[] p=genDoubleArray(limit);
for (int i=0; i<limit; i++)
System.out.println(i+": "+p[i]);
}
}


C语言中的实现:

#include <time.h>
#include <stdlib.h>
#include <stdio.h>
#include <jni.h>
#include "GDA.h"

__attribute ((constructor)) void init_func(){
srand((unsigned)time(NULL));
printf("seed setted!\n");
}

JNIEXPORT jdoubleArray JNICALL
Java_GDA_genDoubleArray (JNIEnv *env, jclass clazz, jint length){
jdoubleArray gen_arr=(*env)->NewDoubleArray(env, length);
if (gen_arr==NULL) {
fprintf(stderr, "cannot create double array!\n");
return NULL;
}
double tmp[length];
for (int i=0; i<length; i++)
tmp[i]=rand()/(double)RAND_MAX;
//		tmp[i]=(double)i;
(*env)->SetDoubleArrayRegion(env, gen_arr, 0, length,tmp);
return gen_arr;
}


运行结果是:

hu@forhu-debian:~/Java code/JNI code/GenDoubleArr$ java GDA
seed setted!
0: 0.5973263795475132
1: 0.29107160646983965
2: 0.20303377891100655
3: 0.3453450050881808
4: 0.21342075206964312
5: 0.19031665855567748
6: 0.8962435400561632
7: 0.7831179601061707
8: 0.8714664293785889
9: 0.8222254104084453


这里用到了JNI的环境接口,注意接口调用的方式,相应的原型是:

ArrayType New<PrimitiveType>Array(JNIEnv *env, jsize length);

void Set<PrimitiveType>ArrayRegion(JNIEnv *env, ArrayType array,
jsize start, jsize len, const NativeType *buf);


注意这些原型调用的方式

四  字符串

这一节编辑字符串。字符串只是稍微有点麻烦,这应该和文字编码有关:

public class Str {
private static native String retStr();
private native String lowerCase(String str);

static {
System.loadLibrary("Str");
}

public static void main (String[] agrs) {
Str a= new Str();
System.out.println(a.lowerCase(retStr()));
}

}


实现:

#include <stdio.h>
#include <stdlib.h>
#include "Str.h"

static void lower_case(char *c){
char tmp=*c;
if (tmp>='A'&&tmp<='Z')
tmp+=('a'-'A');
*c=tmp;
}

JNIEXPORT jstring JNICALL Java_Str_retStr (JNIEnv *env, jclass clazz){
char *hp="HELLO WORLD!";
jstring tmp=(*env)->NewStringUTF(env,hp);
if (tmp==NULL){
fprintf(stderr, "Cannot create jstring!\n");
return NULL;
}
return tmp;
}

JNIEXPORT jstring JNICALL Java
4000
_Str_lowerCase (JNIEnv *env, jobject jo, jstring jstr){
char *cptr;
int len=(int)(*env)->GetStringUTFLength(env,jstr);
char *jcbuf=malloc(sizeof(char)*len+1);
if (jcbuf==NULL) {
fprintf(stderr, "memory allocation error!\n");
return NULL;
}
(*env)->GetStringUTFRegion(env, jstr, 0, len, jcbuf);
jcbuf[len]='\0';
for (int i=0;i<len;i++) {
cptr=jcbuf+i;
lower_case(cptr);
}
jstring jret=(*env)->NewStringUTF(env, jcbuf);
if (jret==NULL) {
fprintf(stderr, "cannot create jstring!\n");
free((void *)jcbuf);
return NULL;
}
free((void *)jcbuf);
return jret;
}
对于C给出来的接口,最好是用UTF编码。否则会用jchar来包装,这个东西长度恒为16bits,对于ascii环境应该不太好,我对编码不太了解,这里做不了深层次的讨论。

字符串这一块我也有些把握不准,比如说'\0'返不返回来呢?或许我应该做个实验,迫于时间这个往后再做。

五  C通过JNI调用Java的方法和静态方法

同样的,下面的是java代码:

public class CallMethod {
private static native String toLowerCase (String str);
private static native int getStr(String str);

static {
System.loadLibrary("CallMethod");
}

public static void main (String[] args) {
System.out.println(toLowerCase("HELLO WORLD!"));
System.out.println(getStr("100"));
}
}


下面的是C实现:

#include <stdio.h>
#include <stdlib.h>
#include "CallMethod.h"

JNIEXPORT jstring JNICALL Java_CallMethod_toLowerCase (JNIEnv *env, jclass clazz, jstring str){

if (str==NULL) return NULL;
//first of all, get the class
jclass jstring_c=(*env)->GetObjectClass(env, str);

//then, obtain method from the class
jmethodID to_lower_case=(*env)->GetMethodID(env, jstring_c,
"toLowerCase", /*want to call toLowerCase()*/
"()Ljava/lang/String;"); /*the signature of the method. read charp 3 for details*/
if (to_lower_case==NULL) return NULL;

//now, we can call the method
jstring retstr=(jstring)(*env)->CallObjectMethod(env, str, to_lower_case);
//there should be some arguments followed if the method has some
if (retstr==NULL) return NULL;

return retstr;
}

JNIEXPORT jint JNICALL Java_CallMethod_getStr (JNIEnv *env, jclass clazz, jstring jstr){

jclass jinteger_c=(*env)->FindClass(env,"java/lang/Integer");
if (jinteger_c==NULL) return 0;

jmethodID parse_int=(*env)->GetStaticMethodID(env, jinteger_c,
"parseInt", "(Ljava/lang/String;)I");
if (parse_int==NULL) return 0;

jint reti=(*env)->CallStaticIntMethod(env, jinteger_c, parse_int, jstr);

return reti;
}


可以跑出下面的运行结果:

hello world!
100


总结下来,C通过jni调用方法和静态方法都是三步走:

方法步骤

先调用

jclass GetObjectClass(JNIEnv *env, jobject obj);
找到对应对象的类,

然后再调用

jmethodID GetMethodID(JNIEnv *env, jclass clazz,
const char *name, const char *sig);


找到需要的方法,这里主义name和sig要匹配,sig的规则在doc中有讲述Type Signatures

然后调用下面任意一个函数来完成调用,这三个方法本质是等同的,只是传入参数的形式不一样而已,躯体区别,doc有讲述

http://docs.oracle.com/javase/7/docs/technotes/guides/jni/spec/functions.html#wp4256

NativeType Call<type>Method(JNIEnv *env, jobject obj,
jmethodID methodID, ...);

NativeType Call<type>MethodA(JNIEnv *env, jobject obj,
jmethodID methodID, const jvalue *args);

NativeType Call<type>MethodV(JNIEnv *env, jobject obj,
jmethodID methodID, va_list args);


静态方法步骤

和方法类似

jclass FindClass(JNIEnv *env, const char *name);
jmethodID GetStaticMethodID(JNIEnv *env, jclass clazz,
const char *name, const char *sig);

NativeType CallStatic<type>Method(JNIEnv *env, jclass clazz,
jmethodID methodID, ...);

NativeType CallStatic<type>MethodA(JNIEnv *env, jclass clazz,
jmethodID methodID, jvalue *args);

NativeType CallStatic<type>MethodV(JNIEnv *env, jclass clazz,
jmethodID methodID, va_list args);


六  另外的topic

。。待续

附录:测试环境和硬件参数

hu@forhu-debian:~/Java code/JNI code/CallMethod$ cat /proc/version
Linux version 3.2.0-4-686-pae (debian-kernel@lists.debian.org) (gcc version 4.6.3 (Debian 4.6.3-14) ) #1 SMP Debian 3.2.51-1
hu@forhu-debian:~/Java code/JNI code/CallMethod$ uname -a
Linux forhu-debian 3.2.0-4-686-pae #1 SMP Debian 3.2.51-1 i686 GNU/Linux
hu@forhu-debian:~/Java code/JNI code/CallMethod$ java -version
java version "1.6.0_27"
OpenJDK Runtime Environment (IcedTea6 1.12.6) (6b27-1.12.6-1~deb7u1)
OpenJDK Client VM (build 20.0-b12, mixed mode, sharing)
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签:  Java JNI