这几天在做一个ubuntu下使用JNI调用c程序的小程序,因为之前对JNI不太了解,其间遇到了不少问题,经过我和同学反复的排查才得以解决。下面总结在linux下使用JNI的一些心得(我用的开发工具是MyEclipse)。
java类TErasureCode主要用来加载.so的c程序,其中方法Encode和Decode与c程序中的编码与解码函数对应,TErasureCode.java代码如下:
package com.ping.ErasureCode; //源码所在包
public class TErasureCode {
static {
System.loadLibrary("ErasureCode"); //载入ErasureCode.so
}
public native void Encode(String file); //本地编码函数,与ErasureCode.so中的编码函数对应
public native void Decode(String file); //本地解码函数,与ErasureCode.so中的解码函数对应
}
将上面的TErasureCode.java编译成.h文件,这里要小心,在linux MyEclipse下我们应该将当前目录cd到工程的src下,使用"javah -jni com.ping.ErasureCode.TErasureCode"指令,使编译出的.h文件名为“com_ping_ErasureCode_TErasureCode.h”,这么做都是因为在MyEclipse下我们将源码都放在了com.ping.ErasureCode包下,否则会提示'UnsatisfiedLinkError"错误。注意:包名中的点“.”会变为下划线"_"。编译好的TErasureCode.h不要去改动它,因为是机器自动生成的,改动后可能会出现连接失败错误,TErasureCode.h的源码如下:
/* DO NOT EDIT THIS FILE - it is machine generated */
#include <jni.h>
/* Header for class com_ping_ErasureCode_TErasureCode */
#ifndef _Included_com_ping_ErasureCode_TErasureCode
#define _Included_com_ping_ErasureCode_TErasureCode
#ifdef __cplusplus
extern "C" {
#endif
/*
* Class: com_ping_ErasureCode_TErasureCode
* Method: Encode
* Signature: (Ljava/lang/String;)V
*/
JNIEXPORT void JNICALL Java_com_ping_ErasureCode_TErasureCode_Encode //编码,点变为了下划线
(JNIEnv *, jobject, jstring);
/*
* Class: com_ping_ErasureCode_TErasureCode
* Method: Decode
* Signature: (Ljava/lang/String;)V
*/
JNIEXPORT void JNICALL Java_com_ping_ErasureCode_TErasureCode_Decode //解码,点变为了下划线
(JNIEnv *, jobject, jstring);
#ifdef __cplusplus
}
#endif
#endif
以上标记红色的函数名要与你的C源码中的函数完全对应,一定不能有差错。我的C程序的部分源码如下:
#include <iostream>
#include "com_ping_ErasureCode_TErasureCode.h" //包含上面编译出的头文件
#include <sys/time.h>
#include <sys/stat.h>
#include <string.h>
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <signal.h>
using namespace std;
............................
int encoder (char *filename){..............}
int decoder (char *filename){..............}
...........................
char* jstringToString(JNIEnv *env, jstring jstr) //将jstring转换为string的方法
{
char* temp = NULL;
jclass clsstring = env->FindClass("java/lang/String");
jstring strencode = env->NewStringUTF("utf-8"); //使用utf-8可以处理中文,但用gb2312却不行,不知为什么
jmethodID mid = env->GetMethodID(clsstring, "getBytes", "(Ljava/lang/String;)[B");
jbyteArray barr= (jbyteArray)env->CallObjectMethod(jstr, mid, strencode);
jsize alen = env->GetArrayLength(barr);
jbyte* ba = env->GetByteArrayElements(barr, JNI_FALSE);
if (alen > 0)
{
temp = (char*)malloc(alen + 1);
memcpy(temp, ba, alen);
temp[alen] = 0;
}
env->ReleaseByteArrayElements(barr, ba, 0);
return temp;
}
JNIEXPORT void JNICALL Java_com_ping_ErasureCode_TErasureCode_Encode(JNIEnv *env, jobject obj, jstring file)//与.h中相同的函数名
{
char* file1 = NULL;
file1 = jstringToString(env, file);
encoder(file1);//调用具体编码函数
}
JNIEXPORT void JNICALL Java_com_ping_ErasureCode_TErasureCode_Decode(JNIEnv *env, jobject obj, jstring file)//与.h中相同的函数名
{
char* file1 = NULL;
file1 = jstringToString(env, file);//调用具体解码函数
decoder(file1);
}
使用下面的指令将ErasureCode.c编译成ErasureCode.so文件:
g++ -I /usr/java/jdk1.6.0_45/include -I /usr/java/jdk1.6.0_45/include/linux -fPIC -c ErasureCode.c
g++ -shared -W1,-soname,libErasureCode.so.1 -o libErasureCode.so.1.0 ErasureCode.o
cp libErasureCode.so.1.0 libErasureCode.so
将生成的.so文件放入系统JDK的安装目录下,具体路径为:/..../jdk1.6.0_45/jre/lib/i386
最后MyEclipse工程中com.ping.ErasureCode包只需要如下的文件:
TErasureCode.java, TErasureCode.class, com_ping_ErasureCode_TErasureCode.h
测试时直接在包中新建一个测试类,new一个TErasureCode对象,在调用其native方法即可,我的测试类如下:
package com.ping.ErasureCode;
public class testEn {
public static void main(String[] args) {
TErasureCode tEn = new TErasureCode();
tEn.Encode(args[0]);//调用native方法
}
}
至此完成!过程中C代码一定要保证没有错误,其间就因为C代码出现数组越界、长度不够或内存泄露的问题,产生不少错误,拖延了项目进度。此外,特别注意的就是源码在包中时,一定要注意包也要加入到.h文件名中,否则MyEclipse不知道从哪里找都相关文件(通常我们认为源码在一个包下,系统就会自动找到同一包中的其他文件,但.h文件好像不是这样的)。
希望能给初用JNI的朋友提供一些帮助,也希望我们能共同探讨更深入的知识。