在Java JNI中获取真正的UTF-8字符

24

在JNI代码中,有没有一种简单的方式将Java字符串转换为真正的UTF-8字节数组?很不幸,GetStringUTFChars() 函数几乎可以实现所需的功能,但并非完全如此,它返回一个"修改"过的UTF-8字节序列。主要区别在于修改后的UTF-8不包含任何空字符(因此您可以将其视为ANSI C空终止字符串),但另一个差异似乎是Unicode补充字符(例如表情符号)的处理方式。

像U+1F604 "微笑着张嘴和眯眼"这样的字符存储为代理对(两个UTF-16字符U+D83D U+DE04),并且具有4字节的UTF-8等效值 F0 9F 98 84,如果我在Java中将字符串转换为UTF-8,则会得到该字节序列:

    char[] c = Character.toChars(0x1F604);
    String s = new String(c);
    System.out.println(s);
    for (int i=0; i<c.length; ++i)
        System.out.println("c["+i+"] = 0x"+Integer.toHexString(c[i]));
    byte[] b = s.getBytes("UTF-8");
    for (int i=0; i<b.length; ++i)
        System.out.println("b["+i+"] = 0x"+Integer.toHexString(b[i] & 0xFF));

上面的代码输出如下:

c[0] = 0xd83d c[1] = 0xde04 b[0] = 0xf0 b[1] = 0x9f b[2] = 0x98 b[3] = 0x84

然而,如果我将's'传递给一个本地JNI方法并调用GetStringUTFChars(),我得到了6个字节。每个代理对字符都被单独转换为一个3字节序列:

JNIEXPORT void JNICALL Java_EmojiTest_nativeTest(JNIEnv *env, jclass cls, jstring _s)
{
    const char* sBytes = env->GetStringUTFChars(_s, NULL);
    for (int i=0; sBytes[i]!=0; ++i)
        fprintf(stderr, "%d: %02x\n", i, sBytes[i]);
    env->ReleaseStringUTFChars(_s, sBytes);
    return result;
}

0: ed 1: a0 2: bd 3: ed 4: b8 5: 84

维基百科的UTF-8文章表明,GetStringUTFChars() 实际上返回CESU-8而不是UTF-8。这反过来会导致我的本地Mac代码崩溃,因为它不是一个有效的UTF-8序列:

CFStringRef str = CFStringCreateWithCString(NULL, path, kCFStringEncodingUTF8);
CFURLRef url = CFURLCreateWithFileSystemPath(NULL, str, kCFURLPOSIXPathStyle, false);

我想我可以把所有的JNI方法都改成接受byte[]而不是String,并在Java中进行UTF-8转换,但这似乎有点丑陋,有更好的解决方案吗?

1个回答

38
这在Java文档中已经明确解释了:

JNI函数

GetStringUTFChars

const char * GetStringUTFChars(JNIEnv *env, jstring string, jboolean *isCopy);

Returns a pointer to an array of bytes representing the string in modified UTF-8 encoding. This array is valid until it is released by ReleaseStringUTFChars().

修改过的UTF-8

JNI使用修改过的UTF-8字符串来表示各种字符串类型。修改过的UTF-8字符串与Java虚拟机使用的相同。修改后的UTF-8字符串被编码,以便仅包含非空ASCII字符的字符序列可以使用每个字符仅一个字节来表示,但是所有Unicode字符都可以表示。
范围在\u0001到\u007F之间的所有字符由单个字节表示,如下所示: table1 字节中的七位数据给出了所表示字符的值。
null字符('\u0000')和范围在'\u0080'到'\u07FF'之间的字符由一对字节x和y表示: table2 字节表示具有值((x & 0x1f) << 6) + (y & 0x3f)的字符。
范围在'\u0800'到'\uFFFF'之间的字符由3个字节x、y和z表示: table3 具有值((x & 0xf) << 12) + ((y & 0x3f) << 6) + (z & 0x3f)的字符由这些字节表示。
代码点高于U+FFFF(称为补充字符)的字符通过分别编码其UTF-16表示的两个代理代码单元来表示。每个代理代码单元由三个字节表示。这意味着,补充字符由六个字节u、v、w、x、y和z表示: table4 具有值0x10000+((v&0x0f)<<16)+((w&0x3f)<<10)+(y&0x0f)<<6)+(z&0x3f)的字符由这六个字节表示。
多字节字符的字节存储在类文件中,按大端(高位字节在前)顺序排列。
此格式与标准UTF-8格式之间有两个区别。首先,空字符(char)0使用双字节格式而不是单字节格式进行编码。这意味着修改过的UTF-8字符串永远不会有嵌入的nulls。其次,仅使用标准UTF-8的一字节、二字节和三字节格式。Java虚拟机不识别标准UTF-8的四字节格式;它使用自己的两倍三字节格式。
有关标准UTF-8格式的更多信息,请参见Unicode标准版本4.0的第3.9节“Unicode编码形式”。
由于U+1F604是一个补充字符,而Java不支持UTF-8的4字节编码格式,因此使用修改后的UTF-8来表示U+1F604,通过每个代理项使用3个字节进行编码,总共6个字节。所以,回答你的问题...
“在JNI代码中,是否有一种简单的方法将Java字符串转换为真正的UTF-8字节数组?”
你可以选择:
  1. Use GetStringChars() to get the original UTF-16 encoded characters, and then create your own UTF-8 byte array from that. The conversion from UTF-16 to UTF-8 is a very simply algorithm to implement by hand, or you can use any pre-existing implementation provided by your platform or 3rd party libraries.

  2. Have your JNI code call back into Java to invoke the String.getBytes(String charsetName) method to encode the jstring object to a UTF-8 byte array, eg:

    JNIEXPORT void JNICALL Java_EmojiTest_nativeTest(JNIEnv *env, jclass cls, jstring _s)
    {
        const jclass stringClass = env->GetObjectClass(_s);
        const jmethodID getBytes = env->GetMethodID(stringClass, "getBytes", "(Ljava/lang/String;)[B");
    
        const jstring charsetName = env->NewStringUTF("UTF-8");
        const jbyteArray stringJbytes = (jbyteArray) env->CallObjectMethod(_s, getBytes, charsetName);
        env->DeleteLocalRef(charsetName);
    
        const jsize length = env->GetArrayLength(stringJbytes);
        const jbyte* pBytes = env->GetByteArrayElements(stringJbytes, NULL); 
    
        for (int i = 0; i < length; ++i)
            fprintf(stderr, "%d: %02x\n", i, pBytes[i]);
    
        env->ReleaseByteArrayElements(stringJbytes, pBytes, JNI_ABORT); 
        env->DeleteLocalRef(stringJbytes);
    }
    
「维基百科的UTF-8文章指出,GetStringUTFChars()实际上返回的是CESU-8而不是UTF-8。」
「Java的Modified UTF-8并不完全相同于CESU-8:」
「CESU-8类似于Java的Modified UTF-8,但没有NUL字符(U+0000)的特殊编码。」

有关如何实现相反转换的任何指针?将本地char *(例如“Hello”)转换为Java字符串? - skboro
1
假设@skboro传入的char*指向真正的UTF-8数据而不是"修改后"的UTF-8数据,则可以采用以下两种方法之一:1)手动将UTF-8解码为UTF-16,然后将其传递给JNI的NewString()函数;2)使用JNI将char数据按原样复制到Java的byte[]数组中,然后将该数组作为输入和字符集名称一起传递给String构造函数,指定字符集为"UTF-8"。 - Remy Lebeau
1
如果char*指向“修改过的”UTF-8数据,则可以直接使用JNI的NewStringUTF()函数。 - Remy Lebeau

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接