【问题标题】:Getting true UTF-8 characters in Java JNI在 Java JNI 中获取真正的 UTF-8 字符
【发布时间】:2015-11-19 05:49:42
【问题描述】:

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

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

诸如 U+1F604 "SMILING FACE WITH OPEN MOUTH AND SMILING EYES" 之类的字符被存储为代理对(两个 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: 编辑 1:a0 2:BD 3:编 4:b8 5:84

Wikipedia UTF-8 article 表明 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 转换,但这似乎有点难看,有更好的解决方案吗?

【问题讨论】:

    标签: java encoding utf-8 java-native-interface


    【解决方案1】:

    这在 Java 文档中有明确的解释:

    JNI Functions

    GetStringUTFChars

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

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

    Modified UTF-8

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

    \u0001\u007F 范围内的所有字符都用一个字节表示,如下所示:

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

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

    字节代表值为((x &amp; 0x1f) &lt;&lt; 6) + (y &amp; 0x3f)的字符。

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

    值为((x &amp; 0xf) &lt;&lt; 12) + ((y &amp; 0x3f) &lt;&lt; 6) + (z &amp; 0x3f)的字符由字节表示。

    码点高于 U+FFFF 的字符(所谓的补充字符)通过分别编码其 UTF-16 表示的两个代理代码单元来表示。每个代理代码单元由三个字节表示。这意味着,补充字符由 u、v、w、x、y 和 z 六个字节表示

    值为0x10000+((v&amp;0x0f)&lt;&lt;16)+((w&amp;0x3f)&lt;&lt;10)+(y&amp;0x0f)&lt;&lt;6)+(z&amp;0x3f)的字符由六个字节表示。

    多字节字符的字节以大端(高字节优先)顺序存储在类文件中。

    此格式与标准 UTF-8 格式有两个不同之处。首先,空字符 (char)0 使用两字节格式而不是一字节格式进行编码。这意味着修改后的 UTF-8 字符串永远不会嵌入空值。其次,仅使用标准 UTF-8 的一字节、二字节和三字节格式。 Java VM 无法识别标准 UTF-8 的四字节格式;它使用自己的两倍三字节格式来代替

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

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

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

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

    您可以:

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

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

      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);
      }
      

    Wikipedia 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 假设 char* 指向真正的 UTF-8 数据而不是“修改”的 UTF-8 数据,或者 1)手动将 UTF-8 解码为 UTF-16,然后将其传递给JNI NewString() 函数,或 2) 使用 JNI 将 char 数据原样复制到 Java byte[] 数组,然后将其传递给 String 构造函数,该构造函数接受 byte[] 和字符集名称作为输入,指定“UTF-8”作为字符集。
    • @skboro 如果char* 指向“修改过的”UTF-8 数据,那么您可以简单地单独使用JNI NewStringUTF() 函数。
    猜你喜欢
    • 2012-09-22
    • 1970-01-01
    • 2018-11-06
    • 2011-10-05
    • 2014-02-05
    • 1970-01-01
    • 1970-01-01
    • 2012-03-31
    • 2016-06-09
    相关资源
    最近更新 更多