Iadul DLL, editia MQL5 : UNICODE vs ANSI

[English version] [MQLmagazine.com in english] [Editia romaneasca]

Cu multi multi ani in urma, cand eram copii, la inceputul anilor ‘90, doua limbaje se bateau in lumea developerilor. Pascal, cu o sintaxa pamanteasca, usor de inteles, potrivita unui limbaj de nivel inalt, si C++, cu o sintaxa mai criptica, dar mai rapida in folosire, potrivita nivelului sau mediu. C++ a castigat batalia, si tot ce s-a facut in Windows a inceput sa fie compilat in C++ si a inceput sa-i poarte semnele: siruri terminate cu null si ce era cunoscut atunci ca si conventia standard de apelare.

Sirurile terminate cu null erau siruri normale, cunoscute ca si siruri ANSI, dar la vremea aceea nu exista UNICODE. Fiecare caracter avea un singur byte iar sirurile aveau o lungime dinamica, intrucat erau presupuse a se termina cu null (un byte zero).
De aceea aplicatiile primeau un pointer care sa indice de unde sa citeasca aceste siruri, si stiau si unde acestea se terminau, cautand un byte zero. Cat despre conventia standard de apelare a procedurilor, compilatorul C++ impingea parametrii pe stiva incepand de la ultimul si terminand cu primul.

Sir terminat cu null (ANSI)

|---------------|
|c1|c2|....| 0  |
|b1|b2|....|bn+1|
|---------------|

Pascal era inversul absolut al C++ in aceste privinte. Sirurile erau si ele tot ANSI, numai un byte per caracter, dar sirurile aveau o lungime fixata de 255 de bytes, sau definita de compilator. Aveau un byte exrtra in fata, specificand lungimea logica a sirului (cati bytes erau efectiv folositi). Cat despre conventia de apelare, aceasta era perfect invers, pentru ca in conventia de apelare pascal, parametrii erau impinsi pe stiva de la primul spre ultimul.

Sir Pascal Standard(ANSI)

|------------------|
|ln|c1|c2|....|c255|
|b1|b2|b3|....|b256|
|------------------|

De aceea sirurile Pascal puteau fi trimise cu totul functiilor, fara nevoia de a se trimite prin referinta, caci acesta este unicul mod prin care trimiterea sirurilor este posibila in C++.

Intrucat C++ a castigat batalia, compilatorul Pascal a trebuit sa se adapteze, si conventia de apelare a fost o sarcina usoara. Cat despre siruri, problema s-a complicat, iar programatorii au trebuit sa se chinuie cu PCHAR, un nume dat tablourilor fixe de cate un byte pe element, care trebuia sa tina siruri C++ terminate cu null care erau trimise prin referinta.

Ca si cum nu era de-ajuns, a aparut standardul UNICODE.

UNICODE este un standard complicat, si nici eu nu-l cunosc in intregime. Diferenta dintre ANSI si UNICODE e aceea ca in UNICODE caracterele sunt mai largi, se intind pe doi bytes fiecare, dar sunt si siruri cu caracterele codate pe 4 bytes. La inceput, sirurile UNICODE pareau ceva nou si ciudat. De aceea, erau numite siruri largi (wide strings). Functiile Windows API care lucrau cu sirurile largi aveau numele terminat in W ; pointerii la siruri ANSI terminate cu null se numeau char* , iar pointerii la siruri UNICODE au trebuit sa fie numite wchar_t*. Cum UNICODE a devenit standard general in timp, acum sirurile UNICODE sunt numite simplu siruri terminate cu null.

Siruri terminate cu null (UNICODE)

|------------------------------------------|
|  c1 |  c2 |....|      cn   |      0      |
|b1|b2|b3|b4|....|b 2n-1|b 2n|b 2n+1|b 2n+2|
|------------------------------------------|

MQL5, ca majoritatea mediilor de programare din prezent, este UNICODE. Chiar si sirurile simple pe care le folosesti in mod regulat sunt UNICODE. Arata ca si cum ar fi ANSI, dar reprezentarea interna e UNICODE. Aceasta din cauza ca ANSI poate fi impachetat in UNICODE, prin umplerea bytes complementari cu 0.

ANSI impachetat in UNICODE (siruri normale MQL5)

|------------------------------------------|
|  c1 |  c2 |....|      cn   |      0      |
|b1|0 |b3|0 |....|b 2n-1| 0  |b 2n+1|  0   |
|b1|b2|b3|b4|....|b 2n-1|b 2n|b 2n+1|b 2n+2|
|------------------------------------------|

Deci, intr-un sir ANSI impachetat in UNCODE, fiecare byte par este 0.
Dar daca ai un DLL C++ mai vechi, care foloseste siruri ANSI terminate cu null.
Aceasta inseamna ca se asteapta si intoarce siruri ANSI terminate cu null.

Deci, daca trimiti un sir “ABC” catre un asemenea DLL, vei avea mapat in bytes : 65, 0, 66, 0, 67, 0.
DLL-ul va vedea primul 0 ca si null-ul care termina sirul si va intelege doar “A” din tot sirul.

Daca e sa primesti “ABC” din acest tip de DLL, vei primi in bytes : 65, 66, 67, 0.
MQL5, fiind UNICODE, va intelege primul caracter ca 65 si 66 (ceva ce pare in chineza) si al doilea caracter ca 67 si 0, mapandu-l in “C”. Apoi va continua sa citeasca, daca nu e nici o eroare access violation pana cand gaseste 0 si 0, care formeaza null-ul, rezultand intr-o pasareasca de neinteles. Eroarrea access violation ar putea sa fie ocolita pentru ca MT5 ar putea aloca suficient spatiu pentru primirea sir.

Din pacate, MQL5 nu are un tip ansistring care sa se ocupe de conversie automat. Totusi, pe partea buna a problemei, cel putin in ambele cazuri sirurile sunt trimise prin referinta, deci e doar o problema de semnificatie decat una de conflict in trimiterea prin valoare sau referinta.

Aceasta inseamna ca trebuie sa trimiti siruri UNICODE care trebuie sa fie decodate corect ca ANSI, si sa primesti siruri ANSI pe care trebuie sa le convertesti la UNICODE pentru a le folosi.

Cand primesti un sir ANSI intr-o forma UNICODE, incepi citirea caracterelor UNICODE, printr-un typecast al fiecarui caracter intr-un unsigned short, apoi imparti acesta in doua ANSI (prin modulo 256), adaugand apoi la UNICODE-ul rezultant mai intai restul (ca si cod ANSI) , si apoi catul (ca si cod ANSI). Asa ca fiecare 2 bytes din ANSI original se mapeaza in 4 bytes (2 caractere UNICODE).

Cand vrei sa impachetezi un UNICODE codat ca ANSI, ca un sir MQL5, ca si ANSI, trebuie sa citesti fiecare doua caractere UNICODE la rand, si fortezi typecast-ul lor in unsigned char, de marimea caracterelor ANSI. Apoi impachetezi noul caracter UNICODE cu primul citit ca rest si al doilea ca si cat intr-un unsigned short mai mare, care se va adauga ca si cod al noului caracter la noul sir rezultant UNICODE.

Ceea ce urmeaza este codul celor doua functii de conversie, scrise ca o biblioteca. Fii atent cand faci acest fisier, sa fie in folderul include, si sa-l salvezi ca stringlib.mqh.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
//+------------------------------------------------------------------+
//|                                                    stringlib.mqh |
//|                                       Copyright Bogdan Caramalac |
//|                                           http://mqlmagazine.com |
//+------------------------------------------------------------------+
#property copyright "Bogdan Caramalac"
#property link      "http://mqlmagazine.com"
 
string ANSI2UNICODE(string s)
  {
   ushort mychar;
   long m,d;
   double mm,dd;
   string img;    
   string res="";
   if (StringLen(s)>0)
     {
      string g=" ";
      for (int i=0;i<StringLen(s);i++)
         {          
          string f="  ";          
          mychar=ushort(StringGetCharacter(s,i));
          mm=MathMod(mychar,256);
          img=DoubleToString(mm,0);
          m=StringToInteger(img);
          dd=(mychar-m)/256;
          img=DoubleToString(dd,0);
          d=StringToInteger(img);
          if (m!=0)
            {
             StringSetCharacter(f,0,ushort(m));
             StringSetCharacter(f,1,ushort(d));
             StringConcatenate(res,res,f);
            }//if (m!=0)
          else
            break;                      
         }//for (int i=0;i<StringLen(s);i++)
      }//if (StringLen(s)>0)
   return(res);
  }
 
string UNICODE2ANSI(string s)
  {
   int leng,ipos;
   uchar m,d;
   ulong big;
   leng=StringLen(s);
   string unichar;
   string res="";
   if (leng!=0)
     {    
      unichar=" ";
      ipos=0;      
      while (ipos<leng)
        { //uchar typecasted because each double byte char is actually one byte
         m=uchar(StringGetCharacter(s,ipos));
         if (ipos+1<leng)
           d=uchar(StringGetCharacter(s,ipos+1));
         else
           d=0;
         big=d*256+m;                
         StringSetCharacter(unichar,0,ushort(big));         
         StringConcatenate(res,res,unichar);    
         ipos=ipos+2;
        }
     }
   return(res);
  }

Cand folosesti biblioteca scrii simplu:

1
#include <stringlib.mqh>

ca in exemplul urmator:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
//+------------------------------------------------------------------+
//|                                                  teststrings.mq5 |
//|                                       Copyright Bogdan Caramalac |
//|                                           http://mqlmagazine.com |
//+------------------------------------------------------------------+
#property copyright "Bogdan Caramalac"
#property link      "http://mqlmagazine.com"
#property version   "1.00"
 
#include <stringlib.mqh>
 
//+------------------------------------------------------------------+
//| Script program start function                                    |
//+------------------------------------------------------------------+
void OnStart()
  {
   string original_unicode,ansi,converted_unicode;
   original_unicode="EvenString";
   ansi=UNICODE2ANSI(original_unicode);
   converted_unicode=ANSI2UNICODE(ansi);
   Print(original_unicode," -> ",ansi," -> ",converted_unicode);
   original_unicode="OddString";
   ansi=UNICODE2ANSI(original_unicode);
   converted_unicode=ANSI2UNICODE(ansi);
   Print(original_unicode," -> ",ansi," -> ",converted_unicode);
  }
//+------------------------------------------------------------------+

One Response to “ Iadul DLL, editia MQL5 : UNICODE vs ANSI ”

  1. Iulian on September 25, 2010 at 1:03 pm

    Bravo…super articol. Mi-ar fi luat ceva pana sa descopar singur asta… Multumesc.

Editii