JNI中文处理问题小结

87103755 2010-07-13

JNI中文处理问题小结

由于工作关系,需要利用JNI在C++与Java程序之间进行方法调用和数据传递,但以前总是在英文环境下工作,对中文(其他语言编码同理)问题反倒没有太关注,最近抽了点时间研究了一下,将自己的体会整理如下,供大家讨论或参考。

在进一步讨论之前,有几点基础知识需要说明:

在Java内部,所有的字符串编码采用的是Unicode即UCS-2。Unicode是用两个字节表示每个字符的字符编码方案。Unicode有一个特性:它包括了世界上所有的字符字形。所以,各个地区的语言都可以建立与Unicode的映射关系,而Java正是利用了这一点以达到异种语言之间的转换;

UTF-8是另一种不同于UCS-2/UCS-4的编码方案,其中UTF代表UCSTransformationFormat,它采用变长的方式进行编码,编码长度可以是1~3(据说理论上最长可以到6,不懂)。

由于UCS-2/UCS-4编码定长的原因,编码产生的字符串会包含一些特殊的字符,如\0(即0x0,所有0~256的字符Unicode编码的第一个字节),这在有些情况下(如传输或解析时)会给我们带来一些麻烦,而且对于一般的英文字母浪费了太多的空间,此外,据说UTF-8还有Unicode所没有的纠错能力(不懂!),因此,Unicode往往只是被用作一种中间码,用于逻辑表示。关于Unicode/UTF-8的更多信息,见参考1;

Java中文乱码问题在很多情况下都可能发生:不同应用间,不同平台间等等,但以上问题已有大量优秀的文章讨论过,这里不作深入探讨,详见参考2、3、4、5。下面简要总结一下:

当我们使用默认编码方式保存源文件时,文件内容实际上是按照我们的系统设定进行编码保存的,这个设定值即file.encoding可以通过下面的程序获得:

publicclassEncoding{

publicstaticvoidmain(String[]args){

System.out.println(System.getProperty("file.encoding"));

}

}

javac在不指定encoding参数时,如果区域设定不正确,则可能造成编/解码错误,这个问题在编译一个从别的环境传过来的文件时可能发生;

2、虽然在Java内部(即运行期间,Runtime)字符串是以Unicode形式存在的,但在class文件中信息是以UTF-8形式存储的(Unicode仅被用作逻辑表示中间码);

对于Web应用,以Tomcat为例,JSP/Servlet引擎提供的JSP转换工具(jspc)搜索JSP文件中用<%@pagecontentType="text/html;charset=<Jsp-charset>"%>指定的charset。如果在JSP文件中未指定<Jsp-charset>,则取系统默认的file.encoding(这个值在中文平台上是GBK),可通过控制面板的RegionalOptions进行修改;jspc用相当于“javac–encoding<Jsp-charset>”的命令解释JSP文件中出现的所有字符,包括中文字符和ASCII字符,然后把这些字符转换成Unicode字符,再转化成UTF-8格式,存为JAVA文件。

我曾经偶然将jsp文件存成UTF-8,而在文件内部使用的charset却是GB2312,结果运行时总是无法正常显示中文,后来转存为默认编码方式才正常。只要文件存储格式与JSP开头的charset设置一致,就都可以正常显示(不过将文件保存成UTF-16的情况下我还没有试验成功);

在XML文件中,encoding表示的是文件本身的编码方式,如果这个参数设定与文件本身实际的编码方式不一致的话,则可能解码失败,所以应该总是将encoding设置成与文件编码方式一致的值;而JSP/HTML的charset则表示按照何种字符集来解码从文件中读取出来的字符串(在理解中文问题时应该把字符串理解成一个二进制或16进制的串,按照不同的charset可能映射成不同的字符)。

我曾经在网上就encoding的具体含义跟别人讨论过:如果encoding指的是文件本身的编码方式,那么读取该文件的应用程序在不知道encoding设置的情况下如何正确解读该文件呢?

根据讨论及个人理解,处理程序(如jspc)总是按ISO8859-1来读取输入文件,然后检查文件开始的几个字节(即ByteOrderMark,BOM,具体如何判断,可以参考Tomcat源码$SOURCE_DIR\jasper\jasper2\src\share\org\apache\jasper\xmlparser\XMLEncodingDetector.java的getEncodingName方法,在JSPSpecification的PageCharacterEncoding一节也有详细论述)以探测文件是以何种格式保存的,当解析到encoding选项时,若encoding设置与文件实际保存格式不一致,会尝试进行转换,但这种转换可能在文件实际以ISO8859-1/UTF-8等单字节编码而encoding被设置成Unicode、UTF-16等双字节编码时发生错误。

下面重点讨论JNI中在C++程序与Java程序间进行数据传递时需要注意的问题。

在JNI中jstring采用的是UCS-2编码,与Java中String的编码方式一致。但是在C++中,字符串是用char(8位)或者wchar_t(16位,Unicode编码与jchar一致,但并非所有开发平台上都是Unicode编码,详见参考6),下面的程序证明了这一点(编译环境:VC6):

#include<iostream>

usingnamespacestd;

intmain()

{

localeloc("Chinese-simplified");

//localeloc("chs");

//localeloc("ZHI");

//localeloc(".936");

wcout.imbue(loc);

wcout<<L"中文"<<endl;//若没有L,会出问题

wchar_twch[]={0x4E2D,0x6587,0x0};//"中文"二字的Unicode编码

wcout<<wch<<endl;

return0;

}

JNI提供了几个方法来实现jstring与char/wchar_t之间的转换。jsizeGetStringLength(jstringstr)

constjchar*GetStringChars(jstringstr,jboolean*isCopy)

voidReleaseStringChars(jstringstr,constjchar*chars)

此外,为了便于以UTF-8方式进行传输、存储,JNI还提供了几个操作UTF格式的方法:jsizeGetStringUTFLength(jstringstr)

constchar*GetStringUTFChars(jstringstr,jboolean*isCopy)

voidReleaseStringUTFChars(jstringstr,constchar*chars)

GetStringChars返回的是Unicode格式的编码串,而GetStringUTFChars返回的是UTF-8格式的编码串。要创建一个jstring,可以用如下方式:jstringNewJString(JNIEnv*env,LPCTSTRstr)

{

if(!env||!str)

return0;

intslen=strlen(str);

jchar*buffer=newjchar[slen];

intlen=MultiByteToWideChar(CP_ACP,0,str,strlen(str),buffer,slen);

if(len>0&&len<slen)

buffer[len]=0;

jstringjs=env->NewString(buffer,len);

delete[]buffer;

returnjs;

}

而要将一个jstring对象转为一个char字符串数组,可以:intJStringToChar(JNIEnv*env,jstringstr,LPTSTRdesc,intdesc_len)

{

intlen=0;

if(desc==NULL||str==NULL)

return-1;

//Checkbuffersize

if(env->GetStringLength(str)*2+1>desc_len)

{

return-2;

}

memset(desc,0,desc_len);

constwchar_t*w_buffer=env->GetStringChars(str,0);

len=WideCharToMultiByte(CP_ACP,0,w_buffer,wcslen(w_buffer)+1,desc,desc_len,NULL,NULL);

env->ReleaseStringChars(str,w_buffer);

if(len>0&&len<desc_len)

desc[len]=0;

returnstrlen(desc);

}

当然,按照上面的分析,你也可以直接将GetStringChars的返回结果作为wchar_t串来进行操作。或者,如果你愿意,你也可以将GetStringUTFChars的结果通过MultiByteToWideChar转换为UCS2编码串,再通过WideCharToMultiByte转换为多字节串。constchar*pstr=env->GetStringUTFChars(str,false);

intnLen=MultiByteToWideChar(CP_UTF8,0,pstr,-1,NULL,NULL);//得到UTF-8编码的字符串长度

LPWSTRlpwsz=newWCHAR[nLen];

MultiByteToWideChar(CP_UTF8,0,pstr,-1,lpwsz,nLen);//转换的结果是UCS2格式的编码串

intnLen1=WideCharToMultiByte(CP_ACP,0,lpwsz,nLen,NULL,NULL,NULL,NULL);

LPSTRlpsz=newCHAR[nLen1];

WideCharToMultiByte(CP_ACP,0,lpwsz,nLen,lpsz,nLen1,NULL,NULL);//将UCS2格式的编码串转换为多字节

cout<<"Out:"<<lpsz<<endl;

delete[]lpwsz;delete[]lpsz;

当然,我相信很少有人想要或者需要这么做。这里需要注意一点,GetStringChars的返回值是jchar,而GetStringUTFChars的返回值是constchar*。除了上面的办法外,当需要经常在jstring和char*之间进行转换时我们还有一个选择,那就是下面的这个类。这个类本来是一个叫RogerS.Reynolds的老外提供的,想法非常棒,但用起来却不太灵光,因为作者将考虑的重心放在UTF格式串上,但在实际操作中,我们往往使用的却是ACP(ANSIcodepage)串。下面是原作者的程序:

classUTFString{

private:

UTFString();//Defaultctor-disallowed

public:

//Createanewinstancefromthespecifiedjstring

UTFString(JNIEnv*env,constjstring&str):

mEnv(env),

mJstr(str),

mUtfChars((char*)mEnv->GetStringUTFChars(mJstr,0)),

mString(mUtfChars){}

//Createanewinstancefromthespecifiedstring

UTFString(JNIEnv*env,conststring&str):

mEnv(env),

mString(str),

mJstr(env->NewStringUTF(str.c_str())),

mUtfChars((char*)mEnv->GetStringUTFChars(mJstr,0)){}

//CreateanewinstanceasacopyofthespecifiedUTFString

UTFString(constUTFString&rhs):

mEnv(rhs.mEnv),

mJstr(mEnv->NewStringUTF(rhs.mUtfChars)),

mUtfChars((char*)mEnv->GetStringUTFChars(mJstr,0)),

mString(mUtfChars){}

//Deletetheinstanceandreleaseallocatedstorage

~UTFString(){mEnv->ReleaseStringUTFChars(mJstr,mUtfChars);}

//assignanewvaluetothisinstancefromthegivenstring

UTFString&operator=(conststring&rhs){

mEnv->ReleaseStringUTFChars(mJstr,mUtfChars);

mJstr=mEnv->NewStringUTF(rhs.c_str());

mUtfChars=(char*)mEnv->GetStringUTFChars(mJstr,0);

mString=mUtfChars;

return*this;

}

//assignanewvaluetothisinstancefromthegivenchar*

UTFString&operator=(constchar*ptr){

mEnv->ReleaseStringUTFChars(mJstr,mUtfChars);

mJstr=mEnv->NewStringUTF(ptr);

mUtfChars=(char*)mEnv->GetStringUTFChars(mJstr,0);

mString=mUtfChars;

return*this;

}

//SupplyoperatormethodsforconvertingtheUTFStringtoastring

//orchar*,makingiteasytopassUTFStringargumentstofunctions

//thatrequirestringorchar*parameters.

string&GetString(){returnmString;}

operatorstring(){returnmString;}

operatorconstchar*(){returnmString.c_str();}

operatorjstring(){returnmJstr;}

private:

JNIEnv*mEnv;//Theenviromentpointerforthisnativemethod.

jstringmJstr;//AcopyofthejstringobjectthatthisUTFStringrepresents

char*mUtfChars;//PointertothedatareturnedbyGetStringUTFChars

stringmString;//stringbufferforholdingthe"value"ofthisinstance

};

我将它改了改:classJNIString{

private:

JNIString();//Defaultctor-disallowed

public:

//Createanewinstancefromthespecifiedjstring

JNIString(JNIEnv*env,constjstring&str):

mEnv(env){

constjchar*w_buffer=env->GetStringChars(str,0);

mJstr=env->NewString(w_buffer,

wcslen(w_buffer));//DeepCopy,inusualcaseweonlyneed

//ShallowCopyaswejustneedthisclassto

//providesomeconvenienceforhandlingjstring

mChars=newchar[wcslen(w_buffer)*2+1];

WideCharToMultiByte(CP_ACP,0,w_buffer,wcslen(w_buffer)+1,mChars,wcslen(w_buffer)*2+1,

NULL,NULL);

env->ReleaseStringChars(str,w_buffer);

mString=mChars;

}

//Createanewinstancefromthespecifiedstring

JNIString(JNIEnv*env,conststring&str):

mEnv(env){

intslen=str.length();

jchar*buffer=newjchar[slen];

intlen=MultiByteToWideChar(CP_ACP,0,str.c_str(),str.length(),buffer,slen);

if(len>0&&len<slen)

buffer[len]=0;

mJstr=env->NewString(buffer,len);

delete[]buffer;

mChars=newchar[str.length()+1];

strcpy(mChars,str.c_str());

mString.empty();

mString=str.c_str();

}

//CreateanewinstanceasacopyofthespecifiedJNIString

JNIString(constJNIString&rhs):

mEnv(rhs.mEnv){

constjchar*wstr=mEnv->GetStringChars(rhs.mJstr,0);

mJstr=mEnv->NewString(wstr,wcslen(wstr));

mEnv->ReleaseStringChars(rhs.mJstr,wstr);

mChars=newchar[strlen(rhs.mChars)+1];

strcpy(mChars,rhs.mChars);

mString=rhs.mString.c_str();

}

//Deletetheinstanceandreleaseallocatedstorage

~JNIString(){delete[]mChars;}

//assignanewvaluetothisinstancefromthegivenstring

JNIString&operator=(conststring&rhs){

delete[]mChars;

intslen=rhs.length();

jchar*buffer=newjchar[slen];

intlen=MultiByteToWideChar(CP_ACP,0,rhs.c_str(),rhs.length(),buffer,slen);

if(len>0&&len<slen)

buffer[len]=0;

mJstr=mEnv->NewString(buffer,len);

delete[]buffer;

mChars=newchar[rhs.length()+1];

strcpy(mChars,rhs.c_str());

mString=rhs.c_str();

return*this;

}

//SupplyoperatormethodsforconvertingtheJNIStringtoastring

//orchar*,makingiteasytopassJNIStringargumentstofunctions

//thatrequirestringorchar*parameters.

string&GetString(){returnmString;}

operatorstring(){returnmString;}

operatorconstchar*(){returnmString.c_str();}

operatorjstring(){returnmJstr;}

private:

JNIEnv*mEnv;//Theenviromentpointerforthisnativemethod.

jstringmJstr;//AcopyofthejstringobjectthatthisJNIStringrepresents

char*mChars;//PointertoaANSIcodepagechararray

stringmString;//stringbufferforholdingthe"value"ofthisinstance(ANSIcodepage)

};

后者除了将面向UTF编码改成了面向ANSI编码外,还去掉了operator=(constchar*ptr)的定义,因为operator=(conststring&rhs)可以在需要的时候替代前者而无需任何额外编码。(因为按照C++规范,constreference可以自动转换,详见本人另一文章“关于constreference的几点说明”)

如果你愿意,给JNIString再加个JNIString(JNIEnv*env,constwstring&str)和一个operator=(constwstring&rhs)操作符重载就比较完美了,:),很简单,留给用得到的朋友自己加吧。

下面是一个使用该类的例子(真正跟用于演示的code很少,大部分都是些routinecode,:)):

#include<iostream>

#include<string>

#include<assert.h>

#include<jni.h>

usingnamespacestd;

intmain(){

intres;

JavaVM*jvm;

JNIEnv*env;

JavaVMInitArgsvm_args;

JavaVMOptionoptions[3];

options[0].optionString="-Djava.compiler=NONE";

options[1].optionString="-Djava.class.path=.;..";//..isspeciallyforthisproject

options[2].optionString="-verbose:jni";

vm_args.version=JNI_VERSION_1_4;

vm_args.nOptions=3;

vm_args.options=options;

vm_args.ignoreUnrecognized=JNI_TRUE;

res=JNI_CreateJavaVM(&jvm,(void**)&env,&vm_args);

if(res<0){

fprintf(stderr,"Can''tcreateJavaVM\n");

return1;

}

jclasscls=env->FindClass("jni/test/Demo");

assert(0!=cls);

jmethodIDmid=env->GetMethodID(cls,"","(Ljava/lang/String;)V");

assert(0!=mid);

wchar_t*p=L"中国";

jobjectobj=env->NewObject(cls,mid,env->NewString(reinterpret_cast(p),wcslen(p)));

assert(0!=obj);

mid=env->GetMethodID(cls,"getMessage","()Ljava/lang/String;");

assert(0!=mid);

jstringstr=(jstring)env->CallObjectMethod(obj,mid);

//useJNIStringforeasierhandling.

JNIStringjnistr(env,str);

cout<<"JNIString:"<<jnistr.GetString()<<endl;

jnistr="中文";

cout<<jnistr.GetString()<<endl;

jvm->DestroyJavaVM();

fprintf(stdout,"JavaVMdestory.\n");

return0;

}

参考资料:

UTF-8andUnicodeFAQforUnix/Linuxs,http://www.cl.cam.ac.uk/~mgk25/unicode.html,

其中文翻译见http://www.linuxforum.net/books/UTF-8-Unicode.html

深入剖析Java编程中的中文问题及建议最优解决方法,http://blog.csdn.net/abnerchai/archive/2004/04/28/18576.aspx

关于Java中文问题的几条分析原则,http://www-900.ibm.com/developerWorks/cn/java/l-javachinese/index.shtml

Java编程技术中汉字问题的分析及解决,http://www-900.ibm.com/developerWorks/cn/java/java_chinese/index.shtml

深入剖析JSP和Servlet对中文的处理过程,http://blog.csdn.net/deuso/archive/2005/12/01/541511.aspx

宽字符标量L"xx"在VC6.0/7.0和GNUg++中的不同实现,http://blog.vckbase.com/smileonce/archive/2004/12/09/1972.html

XMLEncoding,http://www.w3schools.com/xml/xml_encoding.asp

相关推荐