Anwendungen für Unicode anpassen

Aus Appmethod Topics
Wechseln zu: Navigation, Suche

Nach oben zu So compilieren und erzeugen Sie Anwendungen


In diesem Thema werden einige semantische Codekonstrukte beschrieben, die Sie in vorhandenem Quelltext überprüfen sollten, um sicherzustellen, dass Ihre Anwendungen mit dem Typ UnicodeString kompatibel sind. Da Char jetzt WideChar entspricht, und für String per Vorgabe UnicodeString verwendet wird, könnten frühere Annahmen über die Größe in Byte eines Zeichen-Arrays oder Strings jetzt falsch sein.

Allgemeine Informationen über Unicode finden Sie unter Unicode in Appmethod.

Einrichten der Umgebung für die Migration nach Unicode

Suchen Sie nach Quelltext, der:

  • Davon ausgeht, dass SizeOf(Char) 1 ist.
  • Davon ausgeht, dass die Länge (Length) eines Strings gleich der Byte-Anzahl in dem String ist.
  • Strings oder PChars direkt manipuliert.
  • Strings in oder aus einem persistenten Speicher liest oder schreibt.

Die beiden ersten Annahmen sind für Unicode nicht zutreffend, weil bei Unicode SizeOf(Char) größer als 1 Byte ist, und die Länge (Length) eines Strings halb so groß ist wie die Anzahl der Bytes. Darüber hinaus muss Code, der in einen persistenten Speicher schreibt oder daraus liest, gewährleisten, dass die korrekte Anzahl von Bytes geschrieben oder gelesen wird, da ein Zeichen möglicherweise nicht mehr durch ein einzelnes Byte repräsentiert werden kann.

Compiler-Flags

Es wurden Flags hinzugefügt, damit Sie festlegen können, ob String UnicodeString oder AnsiString ist. Damit können Sie Code verwenden, der ältere Versionen von Object Pascal und Appmethod C++ in demselben Quelltext unterstützt. Für den überwiegenden Teil von Quelltext, der Standardoperationen mit Strings durchführt, dürften zwei separate UnicodeString- und AnsiString-Codeabschnitte nicht erforderlich sein. Wenn eine Prozedur jedoch Operationen ausführt, die von der internen Struktur der String-Daten abhängen oder die mit externen Bibliotheken interagieren, könnten separate Codepfade für UnicodeString und AnsiString nötig sein.

Object Pascal

{$IFDEF UNICODE}

C++

 #ifdef _DELPHI_STRING_UNICODE

Compiler-Warnungen

Der Object Pascal-Compiler gibt Warnungen zu Fehlern bei der Typumwandlung (z. B. von UnicodeString oder WideString nach AnsiString oder AnsiChar) aus. Wenn Sie eine Anwendung für Unicode konvertieren, sollten Sie die Warnungen 1057 und 1058 aktivieren, um problematische Bereiche in Ihrem Quelltext zu finden.

Warnungsnummer Text/Name der Warnung

Fehler 1057

Implizite String-Umwandlung von '%s' zu '%s' (IMPLICIT_STRING_CAST)

Fehler 1058

Implizite String-Umwandlung mit potenziellem Datenverlust von '%s' zu '%s' (IMPLICIT_STRING_CAST_LOSS)

Fehler 1059

Explizite String-Umwandlung von '%s' zu '%s' (EXPLICIT_STRING_CAST)

Fehler 1060

Explizite String-Umwandlung mit potenziellem Datenverlust von '%s' zu '%s' (EXPLICIT_STRING_CAST_LOSS)


  • Die Object Pascal-Compiler-Warnungen aktivieren Sie auf der Seite Projekt > Optionen > Compiler-Meldungen.
  • Die C++-Compiler-Warnungen aktivieren Sie auf der Seite Projekt > Optionen > C++-Compiler > Warnungen.

Zu überprüfende Codebereiche

Aufrufe von SizeOf

Überprüfen Sie die Aufrufe von SizeOf für Zeichen-Arrays. Sehen Sie sich das folgende Beispiel an:



var
  Count: Integer;
  Buffer: array[0..MAX_PATH - 1] of Char;
begin
  // Existing code - incorrect when string = UnicodeString
  Count := SizeOf(Buffer);
  GetWindowText(Handle, Buffer, Count);

  // Correct for Unicode
  Count := Length(Buffer); // <<-- Count should be chars not bytes
  GetWindowText(Handle, Buffer, Count);
end;

SizeOf gibt die Größe des Arrays in Byte zurück, aber GetWindowText erwartet, dass Count Zeichen angibt. Verwenden Sie in diesem Fall Length anstelle von SizeOf. Length arbeitet ähnlich für Arrays und Strings. Bei einem Array gibt Length die Anzahl der Array-Elemente zurück, die dem Array zugewiesen sind; bei String-Typen gibt Length die Anzahl der Elemente in dem String zurück.

Verwenden Sie die Funktion StrLen, um die Anzahl der Zeichen in einem nullterminierten String (PAnsiChar oder PWideChar) zu erhalten.

Aufrufe von FillChar

Überprüfen Sie die Aufrufe von FillChar, wenn sie zusammen mit Strings oder Char verwendet werden. Sehen Sie sich den folgenden Quelltext an:

var
  Count: Integer;
  Buffer: array[0..255] of Char;
begin
   // Existing code - incorrect when string = UnicodeString (when char = 2 bytes)
   Count := Length(Buffer);
   FillChar(Buffer, Count, 0);

   // Correct for Unicode
   Count := Length(Buffer) * SizeOf(Char); // <<-- Specify buffer size in bytes
   FillChar(Buffer, Count, 0);
end;

Length gibt die Größe in Elementen zurück, aber FillChar erwartet, dass Count Bytes angibt. In diesem Beispiel sollte Length multipliziert mit der Größe von Char verwendet werden. Außerdem füllt FillChar den String mit Bytes, nicht wie früher mit Char, weil die Standardgröße eines Char jetzt 2 beträgt. Zum Beispiel:



var
  Buf: array[0..32] of Char;
begin
  FillChar(Buf, Length(Buf), #9);
end;

Dieser Code füllt das Array allerdings nicht mit Codepoint $09, sondern mit Codepoint $0909.

Um das erwartete Ergebnis zu erhalten, muss der Code folgendermaßen verändert werden:
var
  Buf: array[0..32] of Char;
begin
  StrPCopy(Buf, StringOfChar(#9, Length(Buf)));
...
end;

Aufrufe von Move

Überprüfen Sie die Aufrufe von Move mit Strings oder Zeichen-Arrays, wie im folgenden Beispiel:



var
   Count: Integer;
   Buf1, Buf2: array[0..255] of Char;
begin
  // Existing code - incorrect when string = UnicodeString (when char = 2 bytes)
  Count := Length(Buf1);
  Move(Buf1, Buf2, Count);

  // Correct for Unicode
  Count := Length(Buf1) * SizeOf(Char); // <<-- Specify buffer size in bytes
  Move(Buf1, Buf2, Count);
end;

Length gibt die Größe in Elementen zurück, aber Move erwartet, dass Count Bytes angibt. In diesem Fall sollte Length multipliziert mit der Größe von Char verwendet werden.

Aufrufe von Read/ReadBuffer-Methoden von TStream

Überprüfen Sie die Aufrufe von TStream.Read/ReadBuffer, wenn Strings oder Zeichen-Arrays verwendet werden. Sehen Sie sich das folgende Beispiel an:



var
  S: string;
  L: Integer;
  Stream: TStream;
  Temp: AnsiString;
begin
  // Existing code - incorrect when string = UnicodeString
  Stream.Read(L, SizeOf(Integer));
  SetLength(S, L);
  Stream.Read(Pointer(S)^, L);

  // Correct for Unicode string data
  Stream.Read(L, SizeOf(Integer));
  SetLength(S, L);
  Stream.Read(Pointer(S)^, L * SizeOf(Char));  // <<-- Specify buffer size in bytes

  // Correct for Ansi string data
  Stream.Read(L, SizeOf(Integer));
  SetLength(Temp, L);              // <<-- Use temporary AnsiString
  Stream.Read(Pointer(Temp)^, L * SizeOf(AnsiChar));  // <<-- Specify buffer size in bytes
  S := Temp;                       // <<-- Widen string to Unicode
end;

Die Lösung hängt vom Format der zu lesenden Daten ab. Mit der Klasse TEncoding können Sie Stream-Text korrekt codieren.

Aufrufe von Write/WriteBuffer-Methoden von TStream

Überprüfen Sie die Aufrufe von TStream.Write/WriteBuffer, wenn Strings oder Zeichen-Arrays verwendet werden. Sehen Sie sich das folgende Beispiel an:


var
  S: string;
  Stream: TStream;
  Temp: AnsiString;
  L: Integer;
begin
  L := Length(S);
  
  // Existing code
  // Incorrect when string = UnicodeString
  Stream.Write(L, SizeOf(Integer)); // Write string length
  Stream.Write(Pointer(S)^, Length(S));
  
  // Correct for Unicode data
  Stream.Write(L, SizeOf(Integer));
  Stream.Write(Pointer(S)^, Length(S) * SizeOf(Char)); // <<-- Specify buffer size in bytes
  
  // Correct for Ansi data
  Stream.Write(L, SizeOf(Integer));
  Temp := S;          // <<-- Use temporary AnsiString
  Stream.Write(Pointer(Temp)^, Length(Temp) * SizeOf(AnsiChar));// <<-- Specify buffer size in bytes
end;

Die korrekte Code hängt vom Format der zu schreibenden Daten ab. Mit der Klasse TEncoding können Sie Stream-Text korrekt codieren.

Aufrufe von GetProcAddress

In Aufrufen der Windows-API-Funktion GetProcAddress sollte immer PAnsiChar verwendet werden, weil es in der Windows-API keine analoge Wide-Funktion gibt. Dieses Beispiel zeigt die korrekte Verwendung:



procedure CallLibraryProc(const LibraryName, ProcName: string);
var
  Handle: THandle;
  RegisterProc: function: HResult stdcall;
begin
  Handle := LoadOleControlLibrary(LibraryName, True);
  @RegisterProc := GetProcAddress(Handle, PAnsiChar(AnsiString(ProcName)));
end;

Aufrufe von RegQueryValueEx

In RegQueryValueEx erhält der Parameter Len die Anzahl der Bytes (nicht der Zeichen) und gibt diese auch zurück. In der Unicode-Version ist daher für den Parameter Len ein doppelt so großer Wert erforderlich.

Hier ist ein Beispiel für einen RegQueryValueEx-Aufruf:

Len := MAX_PATH;
if RegQueryValueEx(reg, PChar(Name), nil, nil, PByte(@Data[0]), @Len) = ERROR_SUCCESS
then
  SetString(Result, Data, Len - 1) // Len includes #0
else
  RaiseLastOSError;


Dieser Code muss folgendermaßen geändert werden:



Len := MAX_PATH * SizeOf(Char);
if RegQueryValueEx(reg, PChar(Name), nil, nil, PByte(@Data[0]), @Len) = ERROR_SUCCES
then
  SetString(Result, Data, Len div SizeOf(Char) - 1) // Len includes #0, Len contains the number of bytes
else
  RaiseLastOSError;

Aufrufe von CreateProcessW

Die Unicode-Version (CreateProcessW) der Windows-API-Funktion CreateProcess verhält sich etwas anders als die ANSI-Version. Folgendes (sinngemäß übersetzt) findet sich in Bezug auf den lpCommandLine-Parameter in MSDN:

"Die Unicode-Version dieser Funktion, CreateProcessW, kann den Inhalt dieses Strings ändern. Daher darf dieser Parameter kein Zeiger auf schreibgeschützten Speicher sein (wie eine const-Variable oder ein literaler String). Wenn dieser Parameter ein Konstanten-String ist, könnte die Funktion eine Zugriffsverletzung verursachen."

Wegen dieses Problems könnte vorhandener Code, der CreateProcess aufruft, eine Zugriffsverletzung verursachen.

Im Folgenden finden Sie einige Beispiele für solchen problematischen Code:



// Passing in a string constant
CreateProcess(nil, 'foo.exe', nil, nil, False, 0,
  nil, nil, StartupInfo, ProcessInfo);
// Passing in a constant expression
  const
    cMyExe = 'foo.exe'
  CreateProcess(nil, cMyExe, nil, nil, False, 0,
    nil, nil, StartupInfo, ProcessInfo);
// Passing in a string whose refcount is -1:
const
  cMyExe = 'foo.exe'
var
  sMyExe: string;
  sMyExe := cMyExe;
  CreateProcess(nil, PChar(sMyExe), nil, nil, False, 0, nil, nil, StartupInfo, ProcessInfo);

Aufrufe von LeadBytes

Früher führte LeadBytes alle Werte auf, die im lokalen System das erste Byte eines Doppelbyte-Zeichens sein konnten. Ersetzen Sie den Quelltext:

if Str[I] in LeadBytes then

durch einem Aufruf der Funktion IsLeadChar:

if IsLeadChar(Str[I]) then

Aufrufe von TMemoryStream

Wenn mit TMemoryStream in eine Textdatei geschrieben wird, sollte am Anfang der Datei ein Byte Order Mark (BOM) eingefügt werden. Hier ein Beispiel für das Schreiben eines BOM in eine Datei:



var
  Bom: TBytes;
begin
  tms: TMemoryStream;
  ...
  Bom := TEncoding.UTF8.GetPreamble;
  tms.Write(Bom[0], Length(Bom));

Code, der in eine Datei schreibt, muss für die Codierung des Unicode-Strings in UTF-8 geändert werden:



var
  Temp: Utf8String;
begin
  tms: TMemoryStream;
  ...
  Temp := Utf8Encode(Str); // Str is string being written to file
  tms.Write(Pointer(Temp)^, Length(Temp));
 //Write(Pointer(Str)^, Length(Str)); original call to write string to file

Aufrufe von MultiByteToWideChar

Aufrufe der Windows-API-Funktion MultiByteToWideChar können einfach durch eine Zuweisung ersetzt werden. Ein Beispiel für MultiByteToWideChar:



procedure TWideCharStrList.AddString(const S: string);
var
  Size, D: Integer;
begin
  Size := Length(S);
  D := (Size + 1) * SizeOf(WideChar);
  FList[FUsed] := AllocMem(D);
  MultiByteToWideChar(0, 0, PChar(S), Size, FList[FUsed], D);
  Inc(FUsed);
end;

Nach der Änderung zu Unicode wurde dieser Aufruf zur Unterstützung der Compilierung unter ANSI und Unicode geändert:



procedure TWideCharStrList.AddString(const S: string);
{$IFNDEF UNICODE}
var
  L, D: Integer;
{$ENDIF}
begin
{$IFDEF UNICODE}
  FList[FUsed] := StrNew(PWideChar(S));
{$ELSE}
  L := Length(S);
  D := (L + 1) * SizeOf(WideChar);
  FList[FUsed] := AllocMem(D);
  MultiByteToWideChar(0, 0, PAnsiChar(S), L, FList[FUsed], D);
{$ENDIF}
  Inc(FUsed);
end;

Aufrufe von SysUtils.AppendStr

AppendStr ist veraltet und für die Verwendung von AnsiString hart codiert, und es ist keine UnicodeString-Überladung verfügbar. Ersetzen Sie Aufrufe wie:

AppendStr(String1, String2);

durch Folgendes:

 String1 := String1 + String2;

Sie können auch die neue Klasse TStringBuilder verwenden.

Verwendung von benannten Threads

Vorhandener Object Pascal-Code, in dem benannte Threads verwendet werden, muss geändert werden. Wenn Sie in früheren Versionen mit dem neuen Eintrag Thread-Objekt der Objektgalerie einen neuen Thread erstellt haben, wurde die folgende Typdeklaration in der Unit des neuen Threads erzeugt:

type
TThreadNameInfo = record
  FType: LongWord; // must be 0x1000
  FName: PChar; // pointer to name (in user address space)
  FThreadID: LongWord; // thread ID (-1 indicates caller thread)
  FFlags: LongWord; // reserved for future use, must be zero
end;


Die Behandlungsroutine für benannte Threads des Debuggers erwartet für FName ANSI-Daten, keine Unicode-Daten, daher muss die obige Deklaration folgendermaßen geändert werden:



type
TThreadNameInfo = record
  FType: LongWord; // must be 0x1000
  FName: PAnsiChar; // pointer to name (in user address space)
  FThreadID: LongWord; // thread ID (-1 indicates caller thread)
  FFlags: LongWord; // reserved for future use, must be zero
end;

Neue benannte Threads werden mit der aktualisierten Typdeklaration erstellt. Nur Code, der in einer früheren Object Pascal-Version erstellt wurde, muss manuell aktualisiert werden.

Wenn Sie in einem Thread-Namen Unicode-Zeichen oder -Strings verwenden möchten, müssen Sie den String in UTF-8 codieren, damit der Debugger ihn korrekt verarbeiten kann. Zum Beispiel:

ThreadNameInfo.FName := UTF8String('UnicodeThread_фис');

Hinweis: In Appmethod C++ verwenden Thread-Objekte immer den korrekten Typ, deshalb besteht dieses Problem für Appmethod C++-Quelltext nicht.

Verwendung von PChar-Typumwandlungen für die Aktivierung der Zeigerarithmetik

In Versionen vor 2009 unterstützten nicht alle Zeigertypen die Zeigerarithmetik. Daher wurden nicht-char Zeiger in PChar umgewandelt, um eine Zeigerarithmetik zu ermöglichen. Jetzt aktivieren Sie die Zeigerarithmetik durch die Verwendung der neuen Compiler-Direktive $POINTERMATH, die ausdrücklich für den Typ PByte vorgesehen ist.

In dem folgenden Codebeispiel werden Zeigerdaten in PChar umgewandelt, damit eine Zeigerarithmetik durchgeführt werden kann:



function TCustomVirtualStringTree.InternalData(Node: PVirtualNode): Pointer;
begin
  if (Node = FRoot) or (Node = nil) then
    Result := nil
  else
    Result := PChar(Node) + FInternalDataOffset;
end;

Sie sollten dies so ändern, dass anstelle von PChar PByte verwendet wird:



function TCustomVirtualStringTree.InternalData(Node: PVirtualNode): Pointer;
begin
  if (Node = FRoot) or (Node = nil) then
    Result := nil
  else
    Result := PByte(Node) + FInternalDataOffset;
end;

In dem obigen Beispiel sind Node keine eigentlichen Zeichendaten. Es wird in PChar umgewandelt, damit über die Zeigerarithmetik auf die Daten zugegriffen werden kann, die sich eine bestimmte Anzahl von Bytes nach Node befinden. Früher hat das funktioniert, weil SizeOf(Char) gleich Sizeof(Byte) war. Das trifft jetzt nicht mehr zu, daher muss solcher Code so geändert werden, dass anstelle von PChar PByte verwendet wird. Ohne diese Änderung zeigt Result auf die falschen Daten.

Variante offene Array-Parameter

Wenn im Quelltext mit TVarRec variante offene Array-Parameter verarbeitet werden, müssen Sie ihn für die Behandlung von UnicodeString erweitern. Für UnicodeString wurde der neue Typ vtUnicodeString definiert. Der Typ vtUnicodeString enthält die UnicodeString-Daten. Das folgende Beispiel zeigt einen Fall, für den neuer Code für die Verarbeitung des UnicodeString-Typs hinzugefügt wurde.



procedure RegisterPropertiesInCategory(const CategoryName: string;
  const Filters: array of const); overload;
var
I: Integer;
begin
  if Assigned(RegisterPropertyInCategoryProc) then
    for I := Low(Filters) to High(Filters) do
      with Filters[I] do
        case vType of
          vtPointer:
            RegisterPropertyInCategoryProc(CategoryName, nil,
              PTypeInfo(vPointer), );
          vtClass:
            RegisterPropertyInCategoryProc(CategoryName, vClass, nil, );
          vtAnsiString:
            RegisterPropertyInCategoryProc(CategoryName, nil, nil,
              string(vAnsiString));
          vtUnicodeString:
            RegisterPropertyInCategoryProc(CategoryName, nil, nil,
              string(vUnicodeString));
        else
          raise Exception.CreateResFmt(@sInvalidFilter, [I, vType]);
        end;
 end;

Weitere zu überprüfende Codebereiche

Suchen Sie nach den folgenden Codekonstrukten, um Probleme bei der Unicode-Aktivierung zu lokalisieren:

  • AllocMem
  • AnsiChar
  • of AnsiChar
  • AnsiString
  • of Char
  • Copy
  • GetMem
  • Length
  • PAnsiChar
  • Pointer
  • Seek
  • ShortString
  • string

Code, der solche Konstrukte enthält, muss für die korrekte Unterstützung des Typs UnicodeString möglicherweise geändert werden.

Siehe auch