アプリケーションを Unicode 対応にする
このトピックでは、アプリケーションが UnicodeString 型と互換性があるようにするために既存のコードを見直す必要がある各種セマンティック コードの構造について説明します。Char は WideChar と、文字列は UnicodeString と等しくなるため、文字配列や文字列のサイズ(バイト単位)に関する以前の仮定が正しくなくなりました。
Unicode に関する一般情報については、「RAD Studio における Unicode」を参照してください。
目次
- 1 Unicode への移行に向けた環境のセットアップ
- 2 確認の必要な特定のコード箇所
- 2.1 SizeOf の呼び出し
- 2.2 FillChar の呼び出し
- 2.3 Move の呼び出し
- 2.4 TStream の Read/ReadBuffer メソッドの呼び出し
- 2.5 TStream の Write/WriteBuffer メソッドの呼び出し
- 2.6 GetProcAddress の呼び出し
- 2.7 RegQueryValueEx の呼び出し
- 2.8 CreateProcessW の呼び出し
- 2.9 LeadBytes の呼び出し
- 2.10 TMemoryStream の呼び出し
- 2.11 MultiByteToWideChar の呼び出し
- 2.12 SysUtils.AppendStr の呼び出し
- 2.13 名前付きスレッドの使用
- 2.14 ポインタの算術演算を有効にするために PChar のキャストを使用する
- 2.15 型可変オープン配列パラメータ
- 2.16 注意が必要な追加コード
- 3 関連項目
Unicode への移行に向けた環境のセットアップ
以下に該当するコードをすべて探します。
SizeOf(Char)を 1 と仮定している。- 文字列の
Lengthが文字列のバイト数に等しいと仮定している。 - 文字列または
PChars値を直接操作している。 - 永続ストレージとの間で文字列を読み書きしている。
Unicode の場合、SizeOf(Char) は 1 バイトより大きく、文字列の Length はバイト数の半分なので、まず、上記の 2 つの仮定は Unicode には当てはまりません。さらに、永続ストレージとの間で読み書きするコードでは、必ず正しいバイト数が読み書きされるようにする必要があります。文字がシングル バイトでは表現できなくなっているおそれがあるからです。
コンパイラ フラグ
string 型が UnicodeString か AnsiString かを指定できるように、フラグが用意されました。これを使用すると、Delphi や C++Builder の旧バージョンをサポートするコードを同じソースで維持することができます。標準的な文字列操作を実行する大半のコードでは、UnicodeString と AnsiString のコード セクションを別にする必要はありません。しかし、文字列データの内部構造に依存する操作や外部ライブラリとやり取りする操作を実行する手続きの場合は、UnicodeString と AnsiString のコード パスを分けなければならない可能性があります。
Delphi の場合
{$IFDEF UNICODE}
C++ の場合
#ifdef _DELPHI_STRING_UNICODE
コンパイラの警告
Delphi コンパイラにはキャストする型のエラーに関連した警告があります(UnicodeString や WideString から AnsiString や AnsiChar になど) アプリケーションを Unicode に変換するとき、コードの問題を検出するために警告 1057 と 1058 を有効にする必要があります。
| 警告番号 | 警告テキスト/名前 |
|---|---|
|
[文字列の暗黙的なキャスト ('%s' から '%s')] (IMPLICIT_STRING_CAST) | |
|
[データ損失の可能性がある文字列の暗黙的なキャスト ('%s' から '%s')] (IMPLICIT_STRING_CAST_LOSS) | |
|
[文字列の明示的なキャスト ('%s' から '%s')] (EXPLICIT_STRING_CAST) | |
|
[データ損失の可能性がある文字列の明示的なキャスト ('%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 の単位はバイトです。この例では、Length に Char のサイズを掛けたものを使用しなければなりません。さらに、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 がバイト単位であることが必要です。この場合、Length に Char のサイズを掛けたものを使用しなければなりません。
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 関数 CreateProcess、CreateProcessW の 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 を処理するために拡張する必要があります。新しい型 vtUnicodeString は UnicodeString 用に定義されます。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 を有効にしたときの問題を見つけるために次のコード構造を検索します。
AllocMemAnsiCharof AnsiCharAnsiStringof CharCopyGetMemLengthPAnsiCharPointerSeekShortStringstring
これらの構造を含むコードは UnicodeString 型を正しくサポートするためには変更する必要があります。