Получение истинных символов UTF-8 в Java JNI

Есть ли простой способ преобразования строки Java в настоящий байтовый массив UTF-8 в коде JNI?

К сожалению, GetStringUTFChars() почти делает то, что требуется, но не совсем, он возвращает "модифицированную" последовательность байтов UTF-8. Основное отличие состоит в том, что модифицированный UTF-8 не содержит никаких нулевых символов (поэтому вы можете рассматривать строку ANSI C с нулевым завершением), но другая разница, похоже, заключается в том, как обрабатываются дополнительные символы Unicode, такие как emoji.

Символ, такой как U + 1F604 "СМОТРЕТЬ ЛИСТЬЮ С ОТКРЫТОМ РИТОМ И СМОТРЕТЬЮ ГЛАЗАМИ", хранится как суррогатная пара (два символа UTF-16 U + D83D U + DE04) и имеет 4-байтовый эквивалент UTF-8 F0 9F 98 84, и это байтовая последовательность, которую я получаю, если я преобразую строку в UTF-8 в Java:

    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, чтобы взять байт [], а не строку и сделать преобразование UTF-8 в Java, но это кажется немного уродливым, есть ли лучшее решение?

Ответ 1

Это ясно объясняется в документации Java:

Функции JNI

GetStringUTFChars

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

Возвращает указатель на массив байтов, представляющий строку в модифицированной кодировке UTF-8. Этот массив действителен до тех пор, пока он не будет выпущен ReleaseStringUTFChars().

Измененный UTF-8

JNI использует измененные строки UTF-8 для представления различных типов строк. Модифицированные строки UTF-8 такие же, как те, что используются виртуальной машиной Java. Измененные строки UTF-8 кодируются так, что последовательности символов, которые содержат только ненулевые символы ASCII, могут быть представлены с использованием только одного байта на символ, но могут быть представлены все символы Юникода.

Все символы в диапазоне от \u0001 до \u007F представлены одним байтом, как показано ниже:

table1

Семь бит данных в байте дают значение представленного символа.

Нулевой символ ('\u0000') и символы в диапазоне от '\u0080' до '\u07FF' представлены парой байтов x и y:

table2

Байты представляют символ со значением ((x & 0x1f) << 6) + (y & 0x3f).

Символы в диапазоне от '\u0800' до '\uFFFF' представлены тремя байтами 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) представлен шестью байтами.

Байты многобайтовых символов хранятся в файле класса в порядке big-endian (старший байт).

Существуют два различия между этим форматом и стандартным форматом UTF-8. Во-первых, нулевой символ (char) 0 кодируется с использованием двухбайтового формата, а не однобайтного формата. Это означает, что измененные строки UTF-8 никогда не имеют встроенных нулей. Во-вторых, используются только однобайтовые, двухбайтовые и трехбайтовые форматы стандартного UTF-8. Java VM не распознает четырехбайтовый формат стандартного UTF-8; вместо этого он использует свой собственный двух-трехбайтовый формат.

Дополнительные сведения о стандартном формате UTF-8 см. в разделе 3.9 Форматы кодировки Unicode стандарта Unicode версии 4.0.

Так как U + 1F604 является дополнительным символом, а Java не поддерживает формат 4-байтового кодирования UTF-8, U + 1F604 представлен в модифицированном UTF-8 путем кодирования суррогатной пары UTF-16 U+D83D U+DE04 с использованием 3 байтов на суррогат, таким образом, 6 байтов.

Итак, чтобы ответить на ваш вопрос...

Есть ли простой способ преобразования строки Java в настоящий байтовый массив UTF-8 в коде JNI?

Вы можете:

  • Используйте GetStringChars(), чтобы получить исходные кодированные символы UTF-16, а затем создайте свой собственный массив байтов UTF-8. Преобразование из UTF-16 в UTF-8 - очень простой алгоритм для реализации вручную.

  • Попросите код 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);
    }
    

Статья в Википедии UTF-8 предполагает, что GetStringUTFChars() фактически возвращает CESU-8, а не UTF-8

Java Modified UTF-8 не совсем то же самое, что CESU-8:

CESU-8 похож на Java Modified UTF-8, но не имеет специального кодирования символа NUL (U + 0000).