アプリケーションを Unicode 対応にする

提供: RAD Studio
移動先: 案内検索

アプリケーションのコンパイル方法とビルド方法 への移動


このトピックでは、アプリケーションが UnicodeString 型と互換性があるようにするために既存のコードを見直す必要がある各種セマンティック コードの構造について説明します。CharWideChar と、文字列は UnicodeString と等しくなるため、文字配列や文字列のサイズ(バイト単位)に関する以前の仮定が正しくなくなりました。

Unicode に関する一般情報については、「RAD Studio における Unicode」を参照してください。

Unicode への移行に向けた環境のセットアップ

以下に該当するコードをすべて探します。

  • SizeOf(Char) を 1 と仮定している。
  • 文字列の Length が文字列のバイト数に等しいと仮定している。
  • 文字列または PChars 値を直接操作している。
  • 永続ストレージとの間で文字列を読み書きしている。

Unicode の場合、SizeOf(Char) は 1 バイトより大きく、文字列の Length はバイト数の半分なので、まず、上記の 2 つの仮定は Unicode には当てはまりません。さらに、永続ストレージとの間で読み書きするコードでは、必ず正しいバイト数が読み書きされるようにする必要があります。文字がシングル バイトでは表現できなくなっているおそれがあるからです。

コンパイラ フラグ

string 型が UnicodeStringAnsiString かを指定できるように、フラグが用意されました。これを使用すると、Delphi や C++Builder の旧バージョンをサポートするコードを同じソースで維持することができます。標準的な文字列操作を実行する大半のコードでは、UnicodeStringAnsiString のコード セクションを別にする必要はありません。しかし、文字列データの内部構造に依存する操作や外部ライブラリとやり取りする操作を実行する手続きの場合は、UnicodeStringAnsiString のコード パスを分けなければならない可能性があります。

Delphi の場合

{$IFDEF UNICODE}

C++ の場合

 #ifdef _DELPHI_STRING_UNICODE

コンパイラの警告

Delphi コンパイラにはキャストする型のエラーに関連した警告があります(UnicodeStringWideString から AnsiStringAnsiChar になど) アプリケーションを Unicode に変換するとき、コードの問題を検出するために警告 1057 と 1058 を有効にする必要があります。

警告番号 警告テキスト/名前

エラー 1057

[文字列の暗黙的なキャスト ('%s' から '%s')] (IMPLICIT_STRING_CAST)

エラー 1058

[データ損失の可能性がある文字列の暗黙的なキャスト ('%s' から '%s')] (IMPLICIT_STRING_CAST_LOSS)

エラー 1059

[文字列の明示的なキャスト ('%s' から '%s')] (EXPLICIT_STRING_CAST)

エラー 1060

[データ損失の可能性がある文字列の明示的なキャスト ('%s' から '%s')] (EXPLICIT_STRING_CAST_LOSS)


  • Delphi コンパイラの警告を有効にするには、 [プロジェクト|オプション...|Delphi コンパイラ|ヒントと警告]に移動します。
  • C++ コンパイラの警告を有効にするには、 [プロジェクト|オプション...|C++ コンパイラ|警告]に移動します。

確認の必要な特定のコード箇所

SizeOf の呼び出し

文字配列に対する SizeOf の呼び出しが正しいかどうかを見直します。以下の例について考えてみましょう。



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 では配列のサイズがバイト単位で返されますが、GetWindowText では Count の単位は文字数です。この例では、SizeOf ではなく、Length を使用する必要があります。Length は、配列でも文字列でも同じように機能します。Length を配列に適用した場合は、配列に割り当てられた配列要素の数が返されます。Length を文字列型に適用した場合は、文字列内の要素の数が返されます。

NULL 終端文字列(PAnsiChar または PWideChar)に含まれている文字の数を取得するには、StrLen 関数を使用します。

FillChar の呼び出し

FillChar を文字列や Char と共に使用している場合は、その呼び出しを見直します。以下のコードについて考えてみましょう。

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 ではサイズが要素数で返されますが、FillChar では Count の単位はバイトです。この例では、LengthChar のサイズを掛けたものを使用しなければなりません。さらに、Char のデフォルト サイズが現在は 2 であるため、FillChar は、以前のような Char ではなくバイト値で文字列を埋めます。以下に例を示します。



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

ただし、このコードでは、コード ポイント $09 ではなくコード ポイント $0909 で配列を埋めます。期待どおりの結果を得るには、コードを以下のように変更する必要があります。

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

Move の呼び出し

以下の例のように、文字列や文字配列での Move の呼び出しを見直します。



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 では要素のサイズを返しますが、Move では Count がバイト単位であることが必要です。この場合、LengthChar のサイズを掛けたものを使用しなければなりません。

TStream の Read/ReadBuffer メソッドの呼び出し

文字列や文字配列が使用されている場合に TStream.Read/ReadBuffer の呼び出しを見直します。以下の例について考えてみましょう。



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;

正しいコードは読み取るデータの形式によって変わります。TEncoding クラスを使用すると、ストリーム テキストを正しくエンコードする場合に便利です。

TStream の Write/WriteBuffer メソッドの呼び出し

文字列や文字配列が使用されている場合に TStream.Write/WriteBuffer の呼び出しを見直します。以下の例について考えてみましょう。


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;

正しいコードは書き込みデータの形式によって変わります。TEncoding クラスを使用すると、ストリーム テキストを正しくエンコードする場合に便利です。

GetProcAddress の呼び出し

Windows API 関数 GetProcAddress の呼び出しは常に PAnsiChar を使用する必要があります。Windows API に対応するワイド関数がないからです。次の例では正しい使用法を示しています。



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;

RegQueryValueEx の呼び出し

RegQueryValueEx では、Len パラメータが文字数ではなく、バイト数を受け取り、返します。したがって、Unicode 版では Len パラメータの 2 倍の値が必要です。

ここに RegQueryValueEx 呼び出しのサンプルを示します。

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;


次のように変更する必要があります。



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;

CreateProcessW の呼び出し

Windows API 関数 CreateProcessCreateProcessW の Unicode 版は ANSI 版とは多少動作が異なります。次に lpCommandLine パラメータについての記事を MSDN から引用します。

「この関数の Unicode 版 CreateProcessW は、この文字列の内容を変更できます。したがって、このパラメータには読み取り専用メモリへのポインタ(定数変数やリテラル文字列など)を指定できません。このパラメータが定数文字列の場合は、関数でアクセス違反が発生します。」

この問題のために、CreateProcess を呼び出す既存のコードはアクセス違反が発生することがあります。

このように問題のあるコードのサンプルを次に示します。



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

LeadBytes の呼び出し

以前は、LeadBytes はローカル システムでダブル バイト文字の最初のバイトになるすべての値を表しました。次の文は、

if Str[I] in LeadBytes then

IsLeadChar 関数の呼び出しで置換します。

if IsLeadChar(Str[I]) then

TMemoryStream の呼び出し

TMemoryStream がテキスト ファイルの書き込みに使用されている場合は、ファイルへの書き込みを開始する前にバイト オーダー マーク(BOM) を書き込むと便利です。BOM をファイルに書き込む例を以下に示します。



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

ファイルに書き込むすべてのコードでは UTF-8 エンコードの Unicode 文字列に変更することが必要です。



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

MultiByteToWideChar の呼び出し

Windows API 関数 MultiByteToWideChar の呼び出しでは、単に割り当てを置換できます。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;

Unicode に変更した後は、この呼び出しは ANSI と Unicode の両方でのコンパイルをサポートするように変更されました。



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;

SysUtils.AppendStr の呼び出し

AppendStr は推奨されていません。AnsiString を使用するようにハードコードされ、UnicodeString のオーバーロードが利用できません。次の文は、

AppendStr(String1, String2);

次のとおり置換します。

 String1 := String1 + String2;

新しい TStringBuilder クラスを使用することもできます。

名前付きスレッドの使用

名前付きスレッドを使用する既存の Delphi コードは変更する必要があります。以前のバージョンでは、ギャラリーで新しいスレッドを作成するために新しい[スレッド オブジェクト]項目を使用したときは、新しいスレッドのユニットで次の型宣言が作成されました。

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;


デバッガの名前付きスレッドのハンドラでは、FName メンバが Unicode ではなく ANSI データであることが必要であり、したがって前の宣言は次のとおり変更する必要があります。



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;

新しい名前付きスレッドは更新された型宣言で作成されます。前のバージョンの Delphi で作成されたコードのみを手動で更新する必要があります。

スレッド名に Unicode 文字や文字列を使用する場合は、デバッガが正しく処理できるように文字列を UTF-8 でエンコードする必要があります。以下に例を示します。

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

メモ: C++Builder のスレッド オブジェクトは常に正しい型を使用しているので、C++Builder のコードでは問題ありません。

ポインタの算術演算を有効にするために PChar のキャストを使用する

2009 より前のバージョンでは、一部のポインタ型でポインタの算術演算がサポートされていませんでした。このため、文字以外の各種ポインタを PChar にキャストすることがポインタの算術演算を有効にするために使用されていました。新しい $POINTERMATH コンパイラ指令を使用してポインタの算術演算が有効になります。PByte 型に対して特に有効です。

ポインタの算術演算を実行するために、ポインタ データを PChar にキャストするコードの例を示します。



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

PChar ではなく PByte を使用するためにこれを変更する必要があります。



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

前の例では、Node は実際には文字データではありません。Node の後に一定のバイト数があるデータにアクセスするためにポインタの算術演算を使用する場合は PChar にキャストします。SizeOf(Char)Sizeof(Byte) と等しいため、以前は動作していました。これは true ではなくなりました。したがって PChar ではなく PByte を使用するためにこれを変更する必要があります。変更しない場合は、Result が不正なデータを指します。

型可変オープン配列パラメータ

型可変オープン配列パラメータを処理するために TVarRec を使用するコードがある場合は、UnicodeString を処理するために拡張する必要があります。新しい型 vtUnicodeStringUnicodeString 用に定義されます。UnicodeString データは型 vtUnicodeString にあります。次の例では、UnicodeString 型を処理するために新しいコードが追加されている場合を示します。



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;

注意が必要な追加コード

Unicode を有効にしたときの問題を見つけるために次のコード構造を検索します。

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

これらの構造を含むコードは UnicodeString 型を正しくサポートするためには変更する必要があります。

関連項目