-——————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:
- 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_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)