まず第一に、これはジェネリックとは関係ありませんが、ジェネリックが使用されている場合に発生する可能性が高くなります。にバッファ オーバーフローのバグがあることが判明しましたTControl.CreateParams
。コードを見ると、TCreateParams
構造体を埋めていることがわかります。特に重要なTCreateParams.WinClassName
のは、現在のクラスの名前 ( ClassName
) を に埋めていることです。残念ながら、charWinClassName
のみの固定長バッファです64
が、NULL ターミネータを含める必要があります。事実上、64
char longClassName
はそのバッファをオーバーフローさせます!
次のコードでテストできます。
TLongWinControlClassName4567890123456789012345678901234567891234 = class(TWinControl)
end;
procedure TForm3.Button1Click(Sender: TObject);
begin
with TLongWinControlClassName4567890123456789012345678901234567891234.Create(Self) do
begin
Parent := Self;
end;
end;
そのクラス名の長さはちょうど64 文字です。1 文字短くすると、エラーがなくなります。
これは、ジェネリックを使用する場合に発生する可能性が非常に高くなります。これは、Delphi が を構築する方法によるものClassName
です。これには、パラメータ タイプが宣言されているユニット名、ドット、およびパラメータ タイプの名前が含まれます。たとえば、TWinControl<TWinControl, TWinControl, TWinControl>
クラスには次の ClassName があります。
TWinControl<Controls.TWinControl,Controls.TWinControl,Controls.TWinControl>
それは75
文字の長さで、63
制限を超えています。
回避策
エラーを生成する可能性のあるクラスから単純なエラー メッセージを採用しました。コンストラクターから、このようなもの:
constructor TWinControl<T, K, W>.Create(aOwner: TComponent);
begin
{$IFOPT D+}
if Length(ClassName) > 63 then raise Exception.Create('The resulting ClassName is too long: ' + ClassName);
{$ENDIF}
inherited;
end;
少なくともこれは、すぐに対処できる適切なエラー メッセージを示しています。
後で編集、真の回避策
前の解決策 (エラーを発生させる) は、非常に長い名前を持つ非ジェネリック クラスに対してはうまく機能します。それを短くして、63文字以下にすることができる可能性が非常に高いでしょう。ジェネリック型の場合はそうではありません。2 つの型パラメーターを受け取る TWinControl の子孫でこの問題に遭遇したため、次の形式でした。
TMyControlName<Type1, Type2>
このジェネリック型に基づく具象型の gnerate ClassName は、次の形式を取ります。
TMyControlName<UnitName1.Type1,UnitName2.Type2>
したがって、5 つの識別子 (2x ユニット識別子 + 3x タイプ識別子) + 5 つのシンボル ( <.,.>
) が含まれます。これら 5 つの識別子の平均の長さは、それぞれ 12 文字未満にする必要があります。そうしないと、合計の長さが 63 を超えます (5x12+5 = 65)。識別子ごとに 11 ~ 12 文字しか使用しないのは非常に少なく、ベスト プラクティスに反します (つまり:キーストロークは自由なので、長い説明的な名前を使用してください!)。繰り返しますが、私の場合、識別子をそれほど短くすることはできませんでした。
を短縮することClassName
が常に可能であるとは限らないことを考慮して、問題の原因 (バッファー オーバーフロー) を取り除こうと考えました。残念ながら、エラーは階層TWinControl.CreateParams
の最下位にあるから発生しているため、これは非常に困難です。ウィンドウ作成パラメータを構築するために継承チェーン全体で使用されるため、呼び出すことはCreateParams
できません。それを呼び出さないと、基本クラスのすべてのコードと中間クラスのすべてのコードを複製する必要があります。また、そのコードのいずれかが の将来のバージョン(またはサブクラス化する可能性のあるサードパーティ コントロールの将来のバージョン) で変更される可能性があるため、移植性も高くありません。inherited
CreateParams
TWinControl.CreateParams
VCL
TWinControl.CreateParams
次の解決策は、バッファーのオーバーフローを停止しませんが、無害にしてから (inherited
呼び出しが返されたときに) 問題を修正します。元のレコードを含む新しいレコードを使用しています(レイアウトを制御できます)が、オーバーフローするTCreateParams
ために多くのスペースが埋め込まれています。次に、完全なテキストを読み取り、レコードの元の境界に収まるように作成し、結果の短縮された名前が一意である可能性が高いことを確認します。一意性の問題を解決するために、元の ClassName の HASH を WndName に含めています。TWinControl.CreateParams
TWinControl.CreateParams
type
TWrappedCreateParamsRecord = record
Orignial: TCreateParams;
SpaceForCreateParamsToSafelyOverflow: array[0..2047] of Char;
end;
procedure TExtraExtraLongWinControlDescendantClassName_0123456789_0123456789_0123456789_0123456789.CreateParams(var Params: TCreateParams);
var Wrapp: TWrappedCreateParamsRecord;
Hashcode: Integer;
HashStr: string;
begin
// Do I need to take special care?
if Length(ClassName) >= Length(Params.WinClassName) then
begin
// Letting the code go through will cause an Access Violation because of the
// Buffer Overflow in TWinControl.CreateParams; Yet we do need to let the
// inherited call go through, or else parent classes don't get the chance
// to manipulate the Params structure. Since we can't fix the root cause (we
// can't stop TWinControl.CreateParams from overflowing), let's make sure the
// overflow will be harmless.
ZeroMemory(@Wrapp, SizeOf(Wrapp));
Move(Params, Wrapp.Orignial, SizeOf(TCreateParams));
// Call the original CreateParams; It'll still overflow, but it'll probably be hurmless since we just
// padded the orginal data structure with a substantial ammount of space.
inherited CreateParams(Wrapp.Orignial);
// The data needs to move back into the "Params" structure, but before we can do that
// we should FIX the overflown buffer. We can't simply trunc it to 64, and we don't want
// the overhead of keeping track of all the variants of this class we might encounter.
// Note: Think of GENERIC classes, where you write this code once, but there might
// be many-many different ClassNames at runtime!
//
// My idea is to FIX this by keeping as much of the original name as possible, but
// including the HASH value of the full name into the window name; If the HASH function
// is any good then the resulting name as a very high probability of being Unique. We'll
// use the default Hash function used for Delphi's generics.
HashCode := TEqualityComparer<string>.Default.GetHashCode(PChar(@Wrapp.Orignial.WinClassName));
HashStr := IntToHex(HashCode, 8);
Move(HashStr[1], Wrapp.Orignial.WinClassName[High(Wrapp.Orignial.WinClassName)-8], 8*SizeOf(Char));
Wrapp.Orignial.WinClassName[High(Wrapp.Orignial.WinClassName)] := #0;
// Move the TCreateParams record back were we've got it from
Move(Wrapp.Orignial, Params, SizeOf(TCreateParams));
end
else
inherited;
end;