JNI中文處理問題小結

來源:互聯網
上載者:User

由於工作關係,需要利用JNI在C++與Java程式之間進行方法調用和資料傳遞,但以前總是在英文環境下工作,對中文(其他語言編碼同理)問題反倒沒有太關注,最近抽了點時間研究了一下,將自己的體會整理如下,供大家討論或參考。
在進一步討論之前,有幾點基礎知識需要說明:

  1. 在Java內部,所有的字串編碼採用的是Unicode即UCS-2。Unicode是用兩個位元組表示每個字元的字元編碼方案。Unicode有一個特
    性:它包括了世界上所有的字元字形。所以,各個地區的語言都可以建立與Unicode的映射關係,而Java正是利用了這一點以達到異種語言之間的轉換;
  2. UTF-8是另一種不同於UCS-2/UCS-4的編碼方案,其中UTF代表UCS Transformation
    Format,它採用變長的方式進行編碼,編碼長度可以是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。下面簡要總結一下:

  1. 當我們使用預設編碼方式儲存源檔案時,檔案內容實際上是按照我們的系統設定進行編碼儲存的,這個設定值即file.encoding可以通過下面的程式獲得:
    public class Encoding {
    public static void main(String[] args) {
    System.out.println(System.getProperty("file.encoding"));
    }
    }

    javac在不指定encoding參數時,如果地區設定不正確,則可能造成編/解碼錯誤,這個問題在編譯一個從別的環境傳過來的檔案時可能發生;

  2. 2、雖然在Java內部(即運行期間,Runtime)字串是以Unicode形式存在的,但在class檔案中資訊是以UTF-8形式儲存的(Unicode僅被用作邏輯表示中間碼)
  3. 對於Web應用,以Tomcat為例,JSP/Servlet引擎提供的JSP轉換工具(jspc)搜尋JSP檔案中用<%@ page
    contentType ="text/html;
    charset=<Jsp-charset>"%>指定的charset。如果在JSP檔案中未指定<Jsp-charset&
    gt;,則取系統預設的file.encoding(這個值在中文平台上是GBK),可通過控制台的Regional
    Options進行修改;jspc用相當於“javac –encoding
    <Jsp-charset>”的命令解釋JSP檔案中出現的所有字元,包括中文字元和ASCII字元,然後把這些字元轉換成Unicode字
    符,再轉化成UTF-8格式,存為JAVA檔案。
    我曾經偶然將jsp檔案存成UTF-8,而在檔案內部使用的charset卻是GB2312,結果運行時總是無法正常顯示中文,後來轉存為預設編碼方式才
    正常。只要檔案儲存體格式與JSP開頭的charset設定一致,就都可以正常顯示(不過將檔案儲存成UTF-16的情況下我還沒有實驗成功);
  4. 在XML檔案中,encoding表示的是檔案本身的編碼方式,如果這個參數設定與檔案本身實際的編碼方式不一致的話,則可能解碼失敗,所以應該總是將
    encoding設定成與檔案編碼方式一致的值;而JSP/HTML的charset則表示按照何種字元集來解碼從檔案中讀取出來的字串(在理解中文問
    題時應該把字串理解成一個二進位或16進位的串,按照不同的charset可能映射成不同的字元)。
    我曾經在網上就encoding的具體含義跟別人討論過:如果encoding指的是檔案本身的編碼方式,那麼讀取該檔案的應用程式在不知道encoding設定的情況下如何正確解讀該檔案呢?
    根據討論及個人理解,處理常式(如jspc)總是按ISO8859-1來讀取輸入檔案,然後檢查檔案開始的幾個位元組(即Byte Order
    Mark,BOM,具體如何判斷,可以參考Tomcat源碼$SOURCE_DIR/jasper/jasper2/src/share/org
    /apache/jasper/xmlparser/XMLEncodingDetector.java的getEncodingName方法,在JSP
    Specification的Page Character
    Encoding一節也有詳細論述)以探測檔案是以何種格式儲存的,當解析到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>
using namespace std;

int main()
{
locale loc( "Chinese-simplified" );
//locale loc( "chs" );
//locale loc( "ZHI" );
//locale loc( ".936" );
wcout.imbue( loc );

wcout << L"中文" << endl; //若沒有L,會出問題

wchar_t wch[] = {0x4E2D, 0x6587, 0x0}; //"中文"二字的Unicode編碼
wcout << wch << endl;

return 0;
}

JNI提供了幾個方法來實現jstring與char/wchar_t之間的轉換。

jsize GetStringLength(jstring str)
const jchar *GetStringChars(jstring str, jboolean *isCopy)
void ReleaseStringChars(jstring str, const jchar *chars)

此外,為了便於以UTF-8方式進行傳輸、儲存,JNI還提供了幾個操作UTF格式的方法:

jsize GetStringUTFLength(jstring str)
const char* GetStringUTFChars(jstring str, jboolean *isCopy)
void ReleaseStringUTFChars(jstring str, const char* chars)

GetStringChars返回的是Unicode格式的編碼串,而GetStringUTFChars返回的是UTF-8格式的編碼串。
要建立一個jstring,可以用如下方式:

jstring NewJString( JNIEnv * env, LPCTSTR str )
{
if (!env || !str)
return 0;

int slen = strlen(str);
jchar * buffer = new jchar[slen];
int len = MultiByteToWideChar(CP_ACP, 0, str, strlen(str), buffer, slen);

if (len > 0 && len < slen)
buffer[len] = 0;

jstring js = env->NewString(buffer, len);
delete [] buffer;
return js;
}

而要將一個jstring對象轉為一個char字串數組,可以:

int JStringToChar( JNIEnv * env, jstring str, LPTSTR desc, int desc_len )
{
int len = 0;

if (desc == NULL || str == NULL)
return -1;

// Check buffer size
if (env->GetStringLength(str) * 2 + 1 > desc_len)
{
return -2;
}
memset(desc, 0, desc_len);

const wchar_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;

return strlen(desc);
}

  當然,按照上面的分析,你也可以直接將GetStringChars的返回結果作為wchar_t串來進行操作。或者,如果你願意,你也可以將
GetStringUTFChars的結果通過MultiByteToWideChar轉換為UCS2編碼串,再通過
WideCharToMultiByte轉換為多位元組串。

const char* pstr = env->GetStringUTFChars(str, false);
int nLen = MultiByteToWideChar( CP_UTF8, 0, pstr, -1, NULL, NULL );//得到UTF-8編碼的字串長度
LPWSTR lpwsz = new WCHAR[nLen];
MultiByteToWideChar( CP_UTF8, 0, pstr, -1, lpwsz, nLen );//轉換的結果是UCS2格式的編碼串
int nLen1 = WideCharToMultiByte( CP_ACP, 0, lpwsz, nLen, NULL, NULL, NULL, NULL );
LPSTR lpsz = new CHAR[nLen1];
WideCharToMultiByte( CP_ACP, 0, lpwsz, nLen, lpsz, nLen1, NULL, NULL );//將UCS2格式的編碼串轉換為多位元組

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

delete [] lpwsz; delete [] lpsz;

  當然,我相信很少有人想要或者需要這麼做。
這裡需要注意一點,GetStringChars的傳回值是jchar,而GetStringUTFChars的傳回值是const char*。
除了上面的辦法外,當需要經常在jstring和char*之間進行轉換時我們還有一個選擇,那就是下面的這個類。這個類本來是一個叫Roger S.
Reynolds的老外提供的,想法非常棒,但用起來卻不太靈光,因為作者將考慮的重心放在UTF格式串上,但在實際操作中,我們往往使用的卻是
ACP(ANSI code page)串。下面是原作者的程式:

class UTFString {
private:

UTFString (); // Default ctor - disallowed

public:

// Create a new instance from the specified jstring
UTFString(JNIEnv* env, const jstring& str) :
mEnv (env),
mJstr (str),
mUtfChars ((char* )mEnv->GetStringUTFChars (mJstr, 0)),
mString (mUtfChars) { }

// Create a new instance from the specified string
UTFString(JNIEnv* env, const string& str) :
mEnv (env),
mString (str),
mJstr (env->NewStringUTF (str.c_str ())),
mUtfChars ((char* )mEnv->GetStringUTFChars (mJstr, 0)) { }

// Create a new instance as a copy of the specified UTFString
UTFString(const UTFString& rhs) :
mEnv (rhs.mEnv),
mJstr (mEnv->NewStringUTF (rhs.mUtfChars)),
mUtfChars ((char* )mEnv->GetStringUTFChars (mJstr, 0)),
mString (mUtfChars) { }

// Delete the instance and release allocated storage
~UTFString() { mEnv->ReleaseStringUTFChars (mJstr, mUtfChars); }

// assign a new value to this instance from the given string
UTFString & operator =(const string& rhs) {
mEnv->ReleaseStringUTFChars (mJstr, mUtfChars);
mJstr = mEnv->NewStringUTF (rhs.c_str ());
mUtfChars = (char* )mEnv->GetStringUTFChars (mJstr, 0);
mString = mUtfChars;
return *this;
}

// assign a new value to this instance from the given char*
UTFString & operator =(const char* ptr) {
mEnv->ReleaseStringUTFChars (mJstr, mUtfChars);
mJstr = mEnv->NewStringUTF (ptr);
mUtfChars = (char* )mEnv->GetStringUTFChars (mJstr, 0);
mString = mUtfChars;
return *this;
}

// Supply operator methods for converting the UTFString to a string
// or char*, making it easy to pass UTFString arguments to functions
// that require string or char* parameters.
string & GetString() { return mString; }
operator string() { return mString; }
operator const char* () { return mString.c_str (); }
operator jstring() { return mJstr; }

private:

JNIEnv* mEnv; // The enviroment pointer for this native method.
jstring mJstr; // A copy of the jstring object that this UTFString represents
char* mUtfChars; // Pointer to the data returned by GetStringUTFChars
string mString; // string buffer for holding the "value" of this instance
};

我將它改了改:

class JNIString {
private:

JNIString (); // Default ctor - disallowed

public:

// Create a new instance from the specified jstring
JNIString(JNIEnv* env, const jstring& str) :
mEnv (env) {
const jchar* w_buffer = env->GetStringChars (str, 0);
mJstr = env->NewString (w_buffer,
wcslen (w_buffer)); // Deep Copy, in usual case we only need
// Shallow Copy as we just need this class to
// provide some convenience for handling jstring

mChars = new char[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;
}

// Create a new instance from the specified string
JNIString(JNIEnv* env, const string& str) :
mEnv (env) {
int slen = str.length ();
jchar* buffer = new jchar[slen];
int len = 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 = new char[str.length () + 1];
strcpy (mChars, str.c_str ());

mString.empty ();
mString = str.c_str ();
}

// Create a new instance as a copy of the specified JNIString
JNIString(const JNIString& rhs) :
mEnv (rhs.mEnv) {
const jchar* wstr = mEnv->GetStringChars (rhs.mJstr, 0);
mJstr = mEnv->NewString (wstr, wcslen (wstr));
mEnv->ReleaseStringChars (rhs.mJstr, wstr);

mChars = new char[strlen (rhs.mChars) + 1];
strcpy (mChars, rhs.mChars);

mString = rhs.mString.c_str ();
}

// Delete the instance and release allocated storage
~JNIString() { delete [] mChars; }

// assign a new value to this instance from the given string
JNIString & operator =(const string& rhs) {
delete [] mChars;

int slen = rhs.length ();
jchar* buffer = new jchar[slen];
int len = 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 = new char[rhs.length () + 1];
strcpy (mChars, rhs.c_str ());

mString = rhs.c_str ();

return *this;
}

// Supply operator methods for converting the JNIString to a string
// or char*, making it easy to pass JNIString arguments to functions
// that require string or char* parameters.
string & GetString() { return mString; }
operator string() { return mString; }
operator const char* () { return mString.c_str (); }
operator jstring() { return mJstr; }

private:

JNIEnv* mEnv; // The enviroment pointer for this native method.
jstring mJstr; // A copy of the jstring object that this JNIString represents
char* mChars; // Pointer to a ANSI code page char array
string mString; // string buffer for holding the "value" of this instance (ANSI code page)
};

  後者除了將面向UTF編碼改成了面向ANSI編碼外,還去掉了operator =(const char* ptr)的定義,因為 operator
=(const string& rhs)可以在需要的時候替代前者而無需任何額外編碼。(因為按照C++規範,const
reference可以自動轉換,詳見本人另一文章“關於 const
reference 的幾點說明”)
  如果你願意,給JNIString再加個JNIString(JNIEnv* env, const wstring& str)和一個operator
=(const wstring& rhs)操作符重載就比較完美了,:),很簡單,留給用得到的朋友自己加吧。
下面是一個使用該類的例子(真正跟用於示範的code很少,大部分都是些routine code,:)):

#include <iostream>
#include <string>
#include <assert.h>
#include <jni.h>

using namespace std;

int main() {
int res;
JavaVM* jvm;
JNIEnv* env;
JavaVMInitArgs vm_args;
JavaVMOption options[3];

options[0].optionString = "-Djava.compiler=NONE";
options[1].optionString = "-Djava.class.path=.;.."; // .. is specially for this project
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''t create Java VM/n");
return 1;
}

jclass cls = env->FindClass ("jni/test/Demo");
assert (0 != cls);

jmethodID mid = env->GetMethodID (cls, "", "(Ljava/lang/String;)V");
assert (0 != mid);

wchar_t* p = L"中國";
jobject obj = 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);

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

// use JNIString for easier handling.
JNIString jnistr (env, str);
cout << "JNIString:" << jnistr.GetString () << endl;

jnistr = "中文";
cout << jnistr.GetString () << endl;

jvm->DestroyJavaVM ();
fprintf (stdout, "Java VM destory./n");

return 0;
}

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.