23

必要なフィールドをいくつか追加できるように、コントロールを順番にサブクラス化しましたが、実行時に作成するとAccess Violation. 残念ながら、このアクセス違反は、コントロールを作成している場所では発生しません。また、すべてのデバッグ オプションを有効にしてビルドしている場合でも (「デバッグ DCU でビルド」を含む)、スタック トレースはまったく役に立ちません。 !

エラーを再現するために、コンソール アプリケーションを作成しようとしましたが、どうやらこのエラーはフォーム アプリケーションにのみ表示され、コントロールが実際にフォームに表示されている場合にのみ表示されます。

エラーを再現する手順は次のとおりです。新しい VCL フォーム アプリケーションを作成し、1 つのボタンをドロップし、ダブルクリックして OnClick ハンドラーを作成し、次のように記述します。

type TWinControl<T,K,W> = class(TWinControl);

procedure TForm3.Button1Click(Sender: TObject);
begin
  with TWinControl<TWinControl, TWinControl, TWinControl>.Create(Self) do
  begin
    Parent := Self;
  end;
end;

これにより、試行するたびにアクセス違反が連続して生成されます。Delphi 2010 でのみこれをテストしました。これは、このコンピューターで入手した唯一のバージョンです。

質問は次のとおりです。

  • これは Delphi の Generics の既知のバグですか?
  • これに対する回避策はありますか?

編集

QC レポートへのリンクは次のとおりです: http://qc.embarcadero.com/wc/qcmain.aspx?d=112101

4

1 に答える 1

27

まず第一に、これはジェネリックとは関係ありませんが、ジェネリックが使用されている場合に発生する可能性が高くなります。にバッファ オーバーフローのバグがあることが判明しましたTControl.CreateParams。コードを見ると、TCreateParams構造体を埋めていることがわかります。特に重要なTCreateParams.WinClassNameのは、現在のクラスの名前 ( ClassName) を に埋めていることです。残念ながら、charWinClassNameのみの固定長バッファです64が、NULL ターミネータを含める必要があります。事実上、64char 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できません。それを呼び出さないと、基本クラスのすべてのコードと中間クラスのすべてのコードを複製する必要があります。また、そのコードのいずれかが の将来のバージョン(またはサブクラス化する可能性のあるサードパーティ コントロールの将来のバージョン) で変更される可能性があるため、移植性も高くありません。inheritedCreateParamsTWinControl.CreateParamsVCL

TWinControl.CreateParams次の解決策は、バッファーのオーバーフローを停止しませんが、無害にしてから (inherited呼び出しが返されたときに) 問題を修正します。元のレコードを含む新しいレコードを使用しています(レイアウトを制御できます)が、オーバーフローするTCreateParamsために多くのスペースが埋め込まれています。次に、完全なテキストを読み取り、レコードの元の境界に収まるように作成し、結果の短縮された名前が一意である可能性が高いことを確認します。一意性の問題を解決するために、元の ClassName の HASH を WndName に含めています。TWinControl.CreateParamsTWinControl.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;
于 2013-01-21T21:03:57.103 に答える