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

有没有一种简单的方法可以在JNI代码中将Java字符串转换为真正的UTF-8字节数组?

不幸的是,GetStringUTFChars()几乎可以完成所需的操作,但并不完全是,它返回一个“修改的”UTF-8字节序列。主要区别在于修改后的 UTF-8 不包含任何空字符(因此您可以处理 ANSI C 空终止字符串),但另一个区别似乎是如何处理 Unicode 增补字符(如表情符号)。

诸如U + 1F604“张开嘴巴和微笑的眼睛的笑脸”之类的字符被存储为代理项对(两个UTF-16字符U + D83D U + DE04),并且具有相当于F0 9F 98 84的4字节UTF-8,如果我在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));

上面的代码打印以下内容:


答案 1

这在 Java 文档中得到了明确的解释:

JNI 函数

GetStringUTFChars

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

返回一个指针,该指针指向一个字节数组,该数组表示采用修改后的 UTF-8 编码的字符串。此数组在 ReleaseStringUTFChars() 发布之前一直有效。

修改后的 UTF-8

JNI 使用修改后的 UTF-8 字符串来表示各种字符串类型。修改后的 UTF-8 字符串与 Java VM 使用的字符串相同。对修改后的 UTF-8 字符串进行编码,以便仅使用每个字符一个字节来表示仅包含非空 ASCII 字符的字符序列,但可以表示所有 Unicode 字符。

范围中的所有字符均由单个字节表示,如下所示:\u0001\u007F

table1

字节中的七位数据给出了所表示的字符的值。

空字符 () 和范围中的字符由一对字节 x 和 y 表示:'\u0000''\u0080''\u07FF'

table2

字节表示值为 的字符。((x & 0x1f) << 6) + (y & 0x3f)

范围中的字符由 3 个字节 x、y 和 z 表示:'\u0800''\uFFFF'

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 字符串永远不会嵌入空值。其次,仅使用标准 UTF-8 的单字节、双字节和三字节格式。Java VM 无法识别标准 UTF-8 的四字节格式。它使用自己的两乘三字节格式。

有关标准 UTF-8 格式的详细信息,请参阅 Unicode 标准 4.0 版的 Unicode 编码形式第 3.9 节。

由于 U+1F604 是增补字符,而 Java 不支持 UTF-8 的 4 字节编码格式,因此 U+1F604 以修改后的 UTF-8 表示,方法是使用每个代理项 3 个字节对对 UTF-16 代理项进行编码,因此总共 6 个字节。U+D83D U+DE04

所以,要回答你的问题...

有没有一种简单的方法可以在JNI代码中将Java字符串转换为真正的UTF-8字节数组?

您可以:

  1. 用于获取原始 UTF-16 编码字符,然后从中创建自己的 UTF-8 字节数组。从 UTF-16 到 UTF-8 的转换是一种非常简单的手动实现算法,或者您可以使用平台或第三方库提供的任何预先存在的实现。GetStringChars()

  2. 让你的JNI代码回调到Java中,调用String.getBytes(String charsetName)方法将对象编码为UTF-8字节数组,例如:jstring

    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) 的特殊编码。


答案 2

推荐