Sull'origine dell'Unicode

From The Joel on Software Translation Project

Revision as of 10:00, 20 November 2009 by Gls (Talk | contribs)
(diff) ← Older revision | Latest revision (diff) | Newer revision → (diff)
Jump to: navigation, search

Mercoledì, 8 Ottobre 2003 Original English Article

Vi siete mai chiesti cosa sia il misterioso tag Content-Type? Quello che si mette nei file HTML e non sapete mai bene cosa debba contenere esattamente? Avete mai ricevuto una mail dai vostri amici in Bulgaria con il subject che riporta "???? ??????? ??? ????" ?

Sono stato sgomento nello scoprire quanti siano gli sviluppatori di software che non sono completamente consapevoli del misterioso mondo dei set di caratteri, degli encoding, dell'Unicode e di quella roba lì.

Un paio di anni fa un beta tester di FogBUZ si domandava se potesse gestire mail in ingresso in giapponese. Giapponese? Esistono mail in giapponese? Non ne avevo idea. Quando guardai meglio il controllo ActiveX commerciale che usavamo per parsare i messaggi email, scoprii che con i set di caratteri faceva esattamente la cosa sbagliata, così dovemmo scrivere del codice eroico che disfacesse le conversioni sbagliate e che le rifacesse daccapo correttamente.

Guardai anche un'altra libreria commerciale, ma anche quella aveva una implementazione dei set di caratteri completamente sbagliata.

Scambiai una rapida corrispondenza con lo sviluppatore di quella libreria e quello mi scrisse che "non potevano farci nulla". Come molti programmatori, semplicemente sperava che le cose che hanno a che fare col testo andassero a posto da sole, in qualche modo.

Non fu così. Quando scoprii che il popolare strumento per il web PHP ha una noncuranza quasi totale per le questioni dell'encoding dei caratteri, usa spensieratamente 8 bit per i caratteri, rendendo quasi impossibile sviluppare applicazioni web che potessero essere internazionalizzate, pensai che era davvero troppo.

Quindi ho un annuncio da fare: se siete programmatori che nel 2003 ancora lavorano senza sapere neanche il minimo a proposito di caratteri, set di caratteri, encoding e Unicode, se vi becco, vi punisco facendovi sbucciare cipolle per 6 mesi in un sottomarino. Giuro.

Un'altra cosa:

NON È COSÌ DIFFICILE

In questo articolo vi istruirò su quello che ogni programmatore che lavora dovrebbe sapere. Tutta quella storia secondo cui testo semplice = ascii = i caratteri sono 8 bit non solo è sbagliata, è sbagliata al di la di ogni speranza e se programmate ancora a quel modo, non siete molto migliori di un medico che non crede ai germi. Per favore non scrivete un'altra riga di codice prima di aver letto questo articolo.

Prima di cominciare vi avverto che se siete una delle rare persone che sanno dell'internazionalizzazione, probabilmete troverete questo articolo un po troppo semplificato. Sto davvero cercando di stabilire un minimo di conoscenza necessaria, così che ognuno possa capire cosa succede e possa scrivere codice che ha almeno una speranza di funzionare in una lingua diversa da quel sottoinsieme dell'Inglese che non comprende neanche le lettere accentate.

E vi avverto che la gestione dei caratteri è solo una piccola parte di quel che ci vuole per creare software che funzioni internazionalmente, ma posso scrivere solo di una cosa per volta, quindi stavolta si tratterà dei set di caratteri.


Contents

Una prospettiva storica

Il modo più facile per comprendere è procedere cronologicamente.

Penserete che adesso mi metto a discutere di ogni set di caratteri dell'antichità, come l'EBCDIC. Bene, non lo farò. L'EBCDIC non è rilevante, ai fini di questo discorso. Non serve tornare tanto indietro nel tempo.

Ai tempi non così antichi, quando lo Unix veniva inventato e K&R stavano scrivendo The C programming language, tutto era semplice. EBCDIC stava per uscire. I soli caratteri che importavano erano le buone vecchie lettere inglesi non accentate e avevamo un codice per quelle, chiamato ASCII, col quale si poteva rappresentare ogni carattere con un numero fra 32 e 127. Lo spazio era il 32, la lettera "A" era il 65, eccetera. Il tutto poteva essere convenientemente gestito occupando solo 7 bit. La gran parte dei computer di allora usava byte a 8 bit, quindi non solo si potevano rappresentare i caratteri ASCII, ma il bit che rimaneva libero, ad essere davvero malvagi, si poteva mettere qualunque cosa si prestesse alle proprie finalità devianti: gli arronzoni di WordStar usarono l'ultimo bit per indicare se la lettera fosse l'ultima della parola, condannando WordStar a non poter scrivere altro che la lingua inglese.

I codici al di sotto del 32 erano chiamati instampabili e venivano usati solo per le imprecazioni. Scherzo. Erano usati come caratteri di controllo, come il 7 che faceva emettere al computer un beep, o il 12 che faceva sì che la pagina corrente fosse trasferita alla stampante per essere stampata.

Ed andava tutto bene, posto che foste anglofoni.

Siccome i byte avevano l'ottavo bit, un sacco di gente pensò "accidenti, possiamo usare i numeri dal 128 al 255 per i nostri scopi". Il problema fu che un sacco digente ebbe la stessa idea nello stesso momento e ognuno aveva la sua opinione su cosa dovesse essere rappresentato con quei numeri fra il 128 e il 255.

Il PC della IBM aveva una cosa che è passata alla storia come il set di caratteri per gli OEM che forniva lettere accentate per le lingue europee e un po di caratteri da disegno: barre verticali, barre orizzontali, barre orizzontali con simpatici pendagli alla estremità destra, e così via; questi simpatici caratteri potevano essere usati per disegnare rettangolini con gli angoli smussati e tante righe, cose che si vedono ancora sui monitor dei vecchi 8086 nelle lavanderie a secco. Infatti, appena la gente cominciò a comprare PC al di fuori degli USA, si cominciò a immaginare i più esotici set di caratteri, naturalmente tutti usavano i codici al di sopra del 128 per i loro scopi. Per esempio su alcuni PC il codice 130 mostrava una é, ma sui computer venduti in Israele mostrava la lettera ebraica Gimel, quindi quando gli americani mandavano i loro résumé in Israele, gli israeliani leggevano r(gimel)sum(gimel). In molti casi, ad esempio nel caso del russo, circolavano diverse idee su cosa fare dei numeri sopra il 128, quindi risultavano non essere interoperabili neanche i documenti scritti in russo.

Alla fine, questi "liberi tutti" per OEM furono codificati nello standard ANSI, ognuno concordò su cosa fare dei numeri sopra il 128, i quali erano sostanzialmente ASCII, ma sui quali circolavano svariate idee su come gestirli, in base a dove si vivesse. Queste differenti convenzioni furono chiamate code pages. Così, ad esempio, il DOS in Israele usava un code page chiamato 862, mentre il DOS greco ne usava uno chiamato 737.

Erano tutti uguali sotto il 128, ma al di sopra di esso tutte le lettere strane resistettero. Le versioni localizzate del DOS avevano decine di questi code pages, che gestivano di tutto, dall'inglese all'islandese e c'erano anche alcuni code pages multilingua che potevano trattare l'esperanto e il galiziano sullo stesso computer! Wow! Ma avere, per dire, l'ebraico e il greco sullo stesso computer era impossibile, a meno di non scriversi in proprio un programma che gestisse il tutto con immagini bitmap, perché ebraico e greco erano code pages differenti che richiedevano che gli stessi numeri sopra il 128 venissero interpretati diversamente.

Nel frattempo, in Asia accadevano cose anche più folli, a causa del fatto che gli alfabeti asiatici hanno migliaia di caratteri per rappresentare i quali 8 bit non sarebbero mai bastati. Questo problema veniva affrontato con un sistema complesso chiamato DBCS, cioè "Double Bytes Character Set" ovvero il set di caratter a doppio byte. In questo standard alcune lettere venivano rappresentate con un byte, altre con due. Era facile muoversi lungo una stringa in avanti, ma tornare indietro era pressoché impossibile. I programmatori erano incoraggiati a non usare s++ e s-- per incrementare e decrementare la posizione lungo una stringa, bensì ad usare funzioni apposite, quali ad esempio la AnsiNext e la AnsiPrev di Windows che erano in grado di gestire la cosa.

Eppure, la gran parte della gente fingeva che un byte fosse un carattere e che un carattere fossero 8 bit e finché non si trasferisse una stringa da un computer ad un altro, o non si usasse più di una lingua, in qualche modo la cosa funzionava. Ma naturalmente, quando arrivò Internet trasferire stringhe da un computer all'altro divenne una cosa piuttosto comune e l'intero sistema rotolò giù come una valanga.

Per fortuna fu inventato lo Unicode.

Unicode

L'Unicode fu uno sforzo eroico di creare un set di caratteri unico che includesse ogni sistema di scrittura del quale si potesse ragionevolmente ipotizzare l'esistenza e anche qualcuno di cui si potesse ragionevolmente escluderla, come il Klingon. Alcuni sono dell'idea che l'Unicode sia un codice a 16 bit in cui ogni lettera viene rappresentata con 16 bit, quindi si possono rappresentare al massimo 65.536 caratteri. Questo, in verità, non è corretto, ma è una idea molto diffusa, se è questo che sapevate, non abbiatevene a male.

In realtà l'Unicode adotta un modo differente di concettualizzare il carattere e questo diverso schema va compreso, se si vuole che l'intero discorso abbia senso.

Fino ad ora, abbiamo assunto che una lettera sia associata ad una certa sequenza di bit che può essere conservata in RAM o sul disco, ad esempio:

 A -> 0100 0001

In Unicode una lettera si associa a un cosiddetto code point che è un'altra astrazione. Come questa astrazione si traduca in una rappresentazione effettiva conservabile in memoria o sul disco è tutta un'altra storia.

In Unicode la lettera A è un ideale platonico. Galleggia nel paradiso delle idee:

 A

Questa A platonica è diversa dalla B ed è diversa dalla a, ma è uguale ad A e ad A. L'idea che una A maiuscola nel font Times New Roman sia uguale ad una A mauiscola nel font Helvetica ma sia diversa da una a minuscola non sembra controversa, ma in alcune lingue stabilire quale lettera sia una certa cosa può essere controverso. La lettera tedesca ß è una vera lettera o solo un modo stravagante di scrivere ss? Se la forma di una lettera cambia a seconda del fatto che la lettera si trovi alla fine di una parola oppure no, siamo in presenza di due lettere diverse? Chi parla ebraico dice si, chi parla arabo dice no. Tuttavia, la gente sveglia del consorzio Unicode ha discusso questo argomento negli ultimi 10 anni, con condimento di una buona dose di dibattito politico di alto profilo, quindi noi possiamo non preoccuparcene. Hanno già capito qual'è il bandolo della matassa.

Ad ogni lettera platonica di ogni alfabeto viene assegnato un numero magico dal consorzio Unicode che si scrive così:

 U+0639

questo numero magico si chiama code point. La U sta per Unicode e i numeri sono esadecimali. U+0639 è la lettera araba Ain. La lettera inglese A sarà U+0041.

Potete indagare su tutti i code point con la utility di mappatura dei caratteri su Windows 2000/XP o visitando il sito dello standard Unicode.

In effetti non c'è un limite alla quantità di lettere che l'Unicode può definire e infatti si è andati ben oltre le 65.536, quindi non tutte le lettere Unicode possono essere tradotte in una sequenza di byte. Ma tanto era un mito.

Ok, quindi adesso abbiamo una stringa:

 Hello

che, in Unicode, corrisponde ad una sequenza di code point che è questa:

 U+0048 U+0065 U+006C U+006C U+006F

Un pugno di code point. Numeri, sostanzialmente. Non abbiamo ancora detto nulla su come conservarli in memoria o su come rappresentarli in una email.

Encoding

È a questo punto che entrano in gioco gli encoding.

La prima idea, che ha condotto al mito dei 16 bit, fu: ehi, perché non rappresentiamo questi numeri con coppie di byte? Così 'hello' diventa:

 00 48 00 65 00 6C 00 6C 00 6F

dove la prima coppia

 00 48

sta per U+0048 e così via.

Giusto? Vediamo: potrebbe essere anche questa?

 48 00 65 00 6C 00 6C 00 6F 00 

Ebbene tecnicamente si, potrebbe, anzi, i primi implementatori volevano assolutamente la facoltà di scegliere se conservare i loro code point in ordine little endian o big endian, a seconda di quale delle due modalità si adattasse meglio alla loro CPU; e fu sera e fu mattina e ci si ritrovò con due standard diversi di conservare i code point in memoria e sui dischi. Così la gente fu costretta ad usare la bizzarra convenzione di anteporre un stringa FE FF all'inizio di ogni stringa Unicode; questo segnale è chiamato Unicode Byte Order Mark che significa, non sorprendentemente, segnale sull'ordine dei byte dell'Unicode. Se l'ordinamento dei vostri byte è inverso il segnale sarà FF FE e chi leggerà la vostra stringa saprà che deve invertire l'ordine.

Accidenti, però non ogni stringa Unicode lì fuori è preceduta da un 'Unicode Byte Order Mark'.


Per un po' sembrò che questa potesse essere una soluzione sufficiente, ma i programmatori cominciavano a lamentarsi: "guarda tutti quegli zeri!" dicevano. Dato che erano tutti americani consideravano testi per lo più in inglese, che raramente impiegavano i code point sopra U+00FF. A dire il vero erano per lo più hippie progressisti californiani con un certa qual tendenza alla conservazione, ahem. Se fossero stati texani non si sarebbero preoccupati di ingozzare i programmi con il doppio dei bytes. Ma quei rammolliti californiani non sopportavano l'idea di raddoppiare lo spazio in memoria dedicato alle stringhe e comunque ormai c'erano tutti quei dannati documenti, là fuori, per i quali erano stati usati i set di caratteri ANSI e DBCS e chi li avrebbe convertiti tutti? Moi? Per questa ragione da sola la gran parte della gente decise di ignorare l'Unicode per alcuni anni e nel frattempole cose peggiorarono.

Così, fu inventato il brillante concetto dell'UTF-8. L'UTF-8 era un altro sistema per conservare le stringhe di code point dell'Unicode, quei numeri magici che cominciavano con U+, impiegando byte da 8 bit. In UTF-8, ogni code point fra 0 e 127 viene conservato in un singolo byte. Solo i code point a partire dal 128 sono conservati usando 2, 3 e fino a 6 bytes.

 Hex Min        Hex Max         Byte Sequence in binary
 00000000	0000007f	0vvvvvvv	
 00000080	000007FF	110vvvvv	10vvvvvv
 00000800	0000FFFF	1110vvvv	10vvvvvv	10vvvvvv
 00010000	001FFFFF	11110vvv	10vvvvvv	10vvvvvv	10vvvvvv
 00200000	03FFFFFF	111110vv	10vvvvvv	10vvvvvv	10vvvvvv	10vvvvvv
 04000000	7FFFFFFF	1110110v	10vvvvvv	10vvvvvv	10vvvvvv	10vvvvvv	10vvvvvv


Questo aveva l'efficace effetto collaterale che il testo ininglese appare esattamente uguale sia che sia codificato in UTF-8 sia codificato in ASCII, così gli americani non notavano niente di strano. Solo nel resto del mondo avrebbero dovuto saltare nei cerchi. Specificamente, 'Hello', che era

 U+0048 U+0065 U+006C U+006C U+006F

sarebbe stato conservato come

 48 65 6C 6C 6F

che, attenzione, è esattamente lo stesso di come sarebbe stato in ASCII, in ANSI e in ogni set di caratteri OEM sul pianeta. Adesso, se foste stati così coraggiosi da usare lettere accentate, o lettere greche, o lettere klingoniane, avreste dovuto usare diversi byte per conservare un solo code point; ma gli americani non si sarebbero accorti di niente. Inoltre, l'UTF-8 ha anche la simpatica proprietà che tutto il codice ignorante di processamento delle stringhe che vuole usare un singolo byte 0 come null o terminatore, non troncherà le stringhe.

Fin qui abbiamno parlato di tre modi di codificare l'Unicode. Quelli tradizionali che impiegano due bytes si chiamano UCS-2 (perché usano due bytes) o UTF-16 (perché usano 16 bit) ma rimane da vedere se impiegano lo schema little endian o quello big endian. Poi c'è il nuovo e popolare standard UTF-8 che ha la apprezzabile proprietà di funzionare abbastanza bene se avete la fortuna di aver a che fare con testi inglesi e programmi stupidi completamente inconsapevoli del fatto che esiste qualcos'altro rispetto all'ASCII.

Ma esistono diversi altri modi di condificare l'Unicode. Ce n'è uno chiamato UTF-7, che assomiglia molto all'UTF-8 ma assicura che ogni carattere venga rappresentato in 7 bit, l'ultimo bit sarà sempre 0, così se dovete far transitare il vostro testo Unicode attraverso qualche sistema di posta elettronica draconiano da stato di polizia che assume che in qualche modo sette bit siano sufficienti per tutto, grazie all'UTF-7 potete farli arrivare a destinazione intatti.

C'è l'UCS-4, che conserva ogni code point in quattro bit, che ha la apprezzabile proprietà di impiegare un numero di bit sempre uguale per ogni code point, ma, sorprendentemente, neanche i texani si spingeranno tanto oltre da impiegare tutta quella memoria.

Ed infatti, adesso che vi state abituando a considerare le lettere come ideali platonici rappresentati come code point dello Unicode, potete considerare anche l'idea che quei code point possano essere tradotti in byte con uno qualunque dei metodi della vecchia scuola ! Per esempio, la stringa Unicode per 'Hello' (ricordiamo: U+0048 U+0065 U+006C U+006C U+006F ) la potremmo codificare in ASCII, o col vecchio encoding greco OEM, o con l'encoding ebraico ANSI, o qualunque altro delle centinaia di encoding che sono stati inventati fino ad oggi, con un solo avvertimento: alcune lettere potrebbero non essere mostrate correttamente.

Se nell'encoding che state usando non esiste un equivalente per il code point dello Unicode che volete rappresentare, generalmente accade che viene mostrato un punto interrogativo '?', oppure, se siete fortunati, un quadratino. Quale vedete, mostrato qui �

Ci sono centinaia di encoding tradizionali che possono conservare correttamente solo alcuni code point e mostrare tutti gli altri come punti interrogativi. Alcuni encoding popolari per il testo in inglese sono il Windows-1252 (che è lo standard per i Windows 9x per le lingue occidentali) e lo ISO-8859-1, detto Latin-1 (anch'esso utile per le lingue occidentali). Ma se usate questi encoding per il russo o l'ebraico otterrete una sfilza di punti interrogativi.

L'UTF-7, l'UTF-8, l'UTF-16 e l'UTF-32 hanno tutti la apprezzabile proprietà di poter rappresentare qualunque code point correttamente.


Il singolo dato più importante riguardo a tutti gli encoding

Se non vi ricordate nulla di quanto avete letto fin qui, per favore cercate di ricordare un singolo fatto estremamente importante: non ha senso avere una stringa senza sapere esattamente quale encoding usa.

Non potete più nascondere la testa sotto la sabbia e fingere che il testo piano sia ASCII.

Il testo piano non esiste.

Se avete una stringa, in memoria, in un file o in un messaggio email, dovete sapere quale encoding impiega altrimenti non potrete interpretarlo o mostrarlo correttamente.

Quasi tutti quegli errori stupidi del tipo "il mio sito web sembra ... " oppure "non riesce a leggere le mie email se uso lettere accentate" risalgono a un programmatore ingenuo che non aveva compreso il fatto semplice che se non mi dici una certa stringa quale encoding impieghi, se l'UTF-8, o l'ASCII, o l'ISO 8859-1 (Latin-1) oppure ancora il Windows 1252 (Europa occidentale), semplicemente io non posso mostrare correttamente la stringa, forse non posso neanche dire dove sia terminata! Ci sono oltre cento diversi encoding, tentare alla cieca non ha senso!

Come trasmettiamo l'informazione su quale encoding usa la nostra stringa? Ci sono modi standard. Per un messaggio email, ci si aspetta che negli header ci sia una riga del tipo

 Content-Type: text/plain; charset="UTF-8"

Per una pagina web l'idea originale era che il server avrebbe dovuto restituire un header simile a quello delle email fra gli header dell'http nel trasmettere la pagina - non all'interno dell'html, ma come uno degli header della response che vengono spediti prima dell'html.

Ma questo causava problemi. Immaginate un grosso server che ospita tanti siti e continaia di pagine create da utenti sparsi intorno al mondo in decine di lingue diverse, ognuno usando l'encoding che la sua copia di FrontPage abbia ritenuto si usare. Il server non avrebbe modo di sapere con quale encoding era stato scritto un file, quindi non lo potrebbe indicare nell'header.

Sarebbe stato opportuno se si fosse potuta piazzare questa indicazione all'interno del sorgente html magari con un tag speciale e apposito. Naturalmente questa cosa ha fatto impazzire i puristi: come si fa a leggere un file prima di sapere quale encoding impiega? Per fortuna quasi tutti gli encoding di uso comune fanno la stessa cosa con i caratteri fra 32 e 127, quindi si può sempre assumere che una pagina web possa cominciare così:

 <html>
 <head>
 <meta http-equiv="Content-Type" content="text/html; charset=utf-8">

senza far apparire segni strani

Ma quel meta tag deve essere la prima cosa che appare nella sezione <head> del file in questione perché il browser che legge il file, appena lo incontra, deve interrompere l'interpretazione e ricominciare, con l'encoding appropriato.

Cosa fanno i web browser quando non trovano nessun Content-Type, né negli header HTTP e neanche fra i meta tag? Internet Explorer fa una cosa interessante: prova a indovinare, basandosi sulla frequenza con cui alcuni byte compaiono tipicamente in alcune lingue con encoding tipicamente usati per quelle lingue. Siccome i vecchi code pages a 8 bit tendevano a mettere le loro lettere nazionali in diverse gamme fra 128 e 255 e siccome ogni lingua umana ha un istogramma caratteristico diverso dell'uso delle lettere, questa soluzione ha in effetti qualche possibilità di funzionare.

È veramente bizzarro, ma sembra che la cosa funzioni abbastanza spesso che gli scrittori di pagine web ingenui che non sanno di aver bisogno di un Content-Type nelle loro pagine, guardano le loro pagine nel browser e vanno bene, finché non capita loro di scrivere qualcosa che non si conforma sufficientemente con le distribuzioni statistiche dei byte della loro combinazione lingua/encoding e Internet Explorer decide che si tratta di coreano e interpreta tutti i caratteri conseguentemente, dando una dimostrazione dal vivo del fatto che la legge di Postel "sii conservatore in quel che emetti e liberale in quel che accetti" francamente non è un buon principio di design.

E cosa farà il visitatore del sito web, che era stato scritto in Bulgaro ma viene interpretato come fosse coreano? Usa il menu View -> Encoding e prova un po di encoding diversi (ce ne sono almeno una dozzina per le lingue europee occidentali) finché l'immagine mostrata dal monitor non lo soddisfa. Se è consapevole di quale sia il problema. La gran parte della gente non lo è.

Per l'ultima versione di CityDesk, il software di pubblicazione di siti web prodotto dalla mia azienda azienda, abbiamo deciso di fare tutto, internamente, con l'Unicode UCS-2 (il codice a due byte), che è quello usato da Visual Basic, COM e Windows NT/2000/XP nativamente. Nel nostro codice C++ dichiariamo le stringhe semplicememnte come wchar_t (char esteso) invece che char e usiamo le funzioni wcs invece di quelle str (per esempio usiamo la wcscat e la wcslen invece delle strcat e strlen). Per creare una sequenza di caratteri da mostrare in C semplicemente si piazza una L che precede, così: L"Hello".

Quando CityDesk pubblica la pagina, converte tutto a UTF-8 che è ben supportato da tutti i browser ormai da anni. E' così che le 29 versioni localizzate di Joel On Software vengono codificate e non ho mai sentito di una sola persona che abbia mai avuto un problema a visualizzarle.

Questo articolo sta diventando lungo e io non posso coprire tutto quel che c'è da sapere sull'encoding dei caratteri e sull'Unicode, ma spero che se avete letto fin qui adesso ne sappiate abbastanza per tornare a programmare, usando gli antibiotici invece di sanguisughe e incantesimi, compito al quale vi lascio volentieri, adesso.

Personal tools