2015-08-25 13 views
10

Esiste un modo semplice per convertire una stringa Java in un vero array di byte UTF-8 nel codice JNI?Ottenimento di caratteri UTF-8 in Java JNI

Sfortunatamente GetStringUTFChars() quasi fa ciò che è richiesto ma non del tutto, restituisce una sequenza di byte UTF-8 "modificata". La differenza principale è che un UTF-8 modificato non contiene alcun carattere null (quindi è possibile trattare una stringa terminata da null ANSI C) ma un'altra differenza sembra essere il modo in cui vengono trattati i caratteri supplementari Unicode come le emoji.

Un carattere come U + 1F604 "FACCIA SORRIDENTE CON BOCCA APERTA E OCCHI SORRIDENTI" è memorizzato come una coppia di surrogati (due caratteri UTF-16 U + D83D U + DE04) e ha un equivalente UTF-8 a 4 byte di F0 9F 98 84, e cioè la sequenza di byte che ricevo se convertire la stringa UTF-8 in 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)); 

il codice sopra stampa il seguente:

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

Tuttavia, 's' se mi passa in un metodo JNI nativo e chiamo GetStringUTFChars() ottengo 6 byte. Ciascuno dei caratteri surrogati al doppino viene convertito a una sequenza di 3 byte indipendentemente:

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

Il Wikipedia UTF-8 article suggerisce che GetStringUTFChars() restituisce effettivamente CESU-8 anziché UTF-8. Che a sua volta fa sì che il mio codice nativo per Mac in crash, perché non è una valida sequenza UTF-8:

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

Suppongo che potrei cambiare tutti i miei metodi JNI per prendere un byte [], piuttosto che una stringa e fare l'UTF -8 conversione in Java ma che sembra un po 'brutta, c'è una soluzione migliore?

risposta

17

Questo è chiaramente spiegato nella documentazione Java:

JNI Functions

GetStringUTFChars

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

restituisce un puntatore a una matrice di byte che rappresenta la stringa in UTF-8 modificato. Questo array è valido finché non viene rilasciato da ReleaseStringUTFChars().

Modified UTF-8

La JNI utilizza modificato stringhe UTF-8 per rappresentare i vari tipi di stringa. Le stringhe UTF-8 modificate sono le stesse utilizzate da Java VM.Le stringhe UTF-8 modificate sono codificate in modo che le sequenze di caratteri che contengono solo caratteri ASCII non nulli possano essere rappresentate utilizzando solo un byte per carattere, ma tutti i caratteri Unicode possono essere rappresentati.

Tutti i caratteri nell'intervallo \u0001 di \u007F sono rappresentati da un singolo byte, come segue:

table1

I sette bit di dati nel byte danno il valore del carattere rappresentato.

Il carattere nullo ('\u0000') e caratteri nell'intervallo '\u0080' di '\u07FF' sono rappresentati da una coppia di byte x ed y:

table2

I byte rappresentano il carattere con il valore ((x & 0x1f) << 6) + (y & 0x3f).

Caratteri nell'intervallo '\u0800' a '\uFFFF' sono rappresentati da 3 byte x, ye z:

table3

Il carattere con il valore ((x & 0xf) << 12) + ((y & 0x3f) << 6) + (z & 0x3f) è rappresentato dai byte.

I caratteri con punti di codice sopra U + FFFF (i cosiddetti caratteri supplementari) sono rappresentati codificando separatamente le due unità di codice surrogato della loro rappresentazione UTF-16. Ciascuna delle unità di codice surrogato è rappresentata da tre byte. Questo significa, caratteri supplementari sono rappresentati da sei byte, u, v, w, x, y, z:

table4

Il carattere con il valore 0x10000+((v&0x0f)<<16)+((w&0x3f)<<10)+(y&0x0f)<<6)+(z&0x3f) è rappresentato dalle sei byte.

I byte di caratteri multibyte sono memorizzati nel file di classe in ordine big-endian (byte alto per primo).

Ci sono due differenze tra questo formato e il formato UTF-8 standard. Innanzitutto, il carattere nullo (char) 0 viene codificato utilizzando il formato a due byte anziché il formato a un byte. Ciò significa che le stringhe UTF-8 modificate non hanno mai valori null incorporati. Secondo, vengono utilizzati solo i formati a un byte, due byte e tre byte di UTF-8 standard. Java VM non riconosce il formato a quattro byte dello standard UTF-8; utilizza il proprio formato due volte tre byte invece.

Per ulteriori informazioni sul formato UTF-8 standard, vedere la sezione 3.9 Forme di codifica Unicode di The Unicode Standard, Versione 4.0.

Da U + 1F604 è un carattere supplementare, e Java non supporta il formato di codifica 4 byte di UTF-8, U + 1F604 è rappresentato in UTF-8 modificato dalla codifica UTF-16 coppia di surrogati U+D83D U+DE04 utilizzando 3 byte per surrogato, quindi 6 byte totali.

Quindi, per rispondere alla tua domanda ...

Esiste un modo semplice per convertire una stringa Java a una vera UTF-8 byte nel codice JNI?

È possibile:

  1. Usa GetStringChars() per ottenere i caratteri originali UTF-16 codificato, e quindi creare il proprio UTF-8 array di byte da questo. La conversione da UTF-16 a UTF-8 è un algoritmo molto semplice da implementare a mano.

  2. avere il vostro codice di chiamata JNI di nuovo in Java per richiamare il metodo String.getBytes(String charsetName) per codificare l'oggetto jstring ad un UTF-8 array di byte, ad esempio:

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

La Wikipedia UTF-8 l'articolo suggerisce che GetStringUTFChars() restituisce in realtà CESU-8 anziché UTF-8

L'UTF-8 modificato di Java non è esattamente la stessa di CESU-8:

CESU-8 è simile a UTF-8 modificata di Java, ma non ha la speciale codifica del carattere NUL (U + 0000).