: MSDN Magazine November 2007

November 2007
NOVEMBER 2007 ボリューム 21 番号 10

• 信頼できるコンピューティング: より安全なソフトウェアの構築を目指した 5 年間の教訓
Michael Howard

• 速習講座: クラッシュを分析してアプリケーションにおけるセキュリティの脆弱性を検出する
A. Abouchaev、D. Hasse、S. Lambert、および G. Wroblewski

• コード レビュー: アプリケーションの出荷前に脆弱性を見つけて修正する
M. Chmielewski、N. Clift、S. Fonrobert、および T. Ostwald

• ファジー テスト: チーム システム向けにカスタム テスト インターフェイス プロバイダを作成する
Dan Griffin

Code Download (511 KB) HTMLHelp Format
コラム

• CLR 徹底解剖: オブジェクトの有効期間を管理する
Tim Fischer

• 基本的な本能: 拡張メソッド
Adrian "Spotty" Bowles

• Office Space: SharePoint 2007 のイベント
Ted Pattison

• Cutting Edge: AJAX におけるユーザー エクスペリエンスを管理する
Dino Esposito

• チーム システム: カスタム チェックイン ポリシー
Brian A. Randell

• 基礎: WCF での同期コンテキスト
Juval Lowy

• Bugslayer: ビュー ステートの影響を測定する
John Robbins

• 同時実行の問題: C# で簡素化された APM
Jeffrey Richter

• .NET の問題: ファイナライザをデバッグする
Stephen Toub

• { End Bracket }: 開発者向けでもある WPF
Kevin Moore

各出版物

• 編集者のメモ: 極端な方法に陥る理由
Howard Dierking

• ツールボックス: 更新されたグリッド コントロール、オープンソースの比較ツール、その他
Scott Mitchell


信頼できるコンピューティング: より安全なソフトウェアの構築を目指した 5 年間の教訓

信頼できるコンピューティング

より安全なソフトウェアの構築を目指した 5 年間の教訓

Michael Howard
この記事の内容 : :
  • 年齢によるコードの優先順位付け
  • 分析ツールおよび自動化の使用
  • 脅威の多角的な検証
  • 教育の重要性
コンテンツ
コードだけが問題ではない
古いコードから修正する
機能の廃止、削除、根絶
ツールの適切な利用
自動化
セキュリティ脆弱性をゼロにすることはできない
セキュリティは終わりのない戦いである
セキュリティの特効薬はない
「目玉の数」の名言は正しい
今日のサービス拒否は明日の悪用
最後に

5 年前、Bill Gates はマイクロソフトの全従業員にメモを送り、より安全なソフトウェアを構築することの重要性を説きました。以来、マイクロソフトの多くの従業員は、担当製品のセキュリティ向上に取り組んできました。その過程で、より安全なソフトウェアを構築するためには何が必要なのかについて、多くの教訓を得ることができました。

セキュリティは静的な領域ではありません。攻撃者が攻撃を仕掛け、防御者がそれを防御し、両者が互いのテクニックについて学ぶなかで、セキュリティは絶えず進化しています。セキュリティはいわば軍備拡大競争です。常に相手よりも優位に立って攻撃を予測するために、私たち防御者は自らの失敗に学び、ユーザーを攻撃から守るためのより良い方法を作り出す必要があります。

では、私たちは過去 5 年間に何を学んだのでしょうか。実際のところ、これらの教訓のほとんどは、きわめてあたりまえのものです。しかし、明白な事柄の多くがそうであるように、誰かに指摘してもらったほうがわかりやすいこともあります。

コードだけが問題ではない

ソフトウェア業界 (より正確には、ソフトウェア品質業界) は、コードを正しく仕上げることに執着しています。そのことに特に異議はありませんが、セキュリティ脆弱性の多くはコーディングの問題ではありません。これらの脆弱性の多くは設計の問題です。セキュリティの問題をコード内で探しているだけだと、全体的な脆弱性を見逃してしまいます。このことは、マイクロソフトがセキュリティ開発ライフサイクル (SDL) プロセスの一部として、脅威のモデル化と攻撃対象分析を義務付ける理由の 1 つです。脅威のモデル化は、製品の設計上の弱点を識別および緩和するために役立つ分析テクニックです。攻撃対象分析は、ソフトウェア製品のどの部分が、ローカルまたはリモートの信頼できないユーザーに公開されているかという点に着目した分析です。攻撃対象が広い製品は、攻撃対象が狭い製品よりも、多くのコードを信頼できないユーザーに公開しています (詳細については、 msdn.microsoft.com/msdnmag/issues/04/11/AttackSurface を参照してください)。

Microsoft Security Bulletin MS07-029 (microsoft.com/technet/security/Bulletin/MS07-029.mspx を参照) で説明されている、DNS RPC バッファ オーバーランという脆弱性は、攻撃者が対象のシステムの制御を完全に奪うことを可能にするものでした。このケースでは、コードに問題があったことは確かです。しかしこのコードは、本来は管理者のみが使用できるようにする必要があるのに、実際には匿名ユーザーやリモート ユーザーがアクセスできるようになっていました。したがって問題は、コードと設計上の脆弱性の組み合わせといえます。この脆弱性については、SDL ブログで分析しています (blogs.msdn.com/sdl/archive/2007/06/28/lessons-learned-from-ms07-029-the-dns-rpc-interface-buffer-overrun.aspx を参照)。

得られた教訓 設計上の潜在的な弱点を明らかにし、ソフトウェアの攻撃対象を見極めるには、脅威モデルを構築することが不可欠です。すべての物理的脅威を緩和し、攻撃対象を可能な限り狭くする必要があります。

古いコードから修正する

コードのレビューを行う場合、私は脆弱性が存在する可能性によってコードをランク付けしています。コード レビューの優先順位を決めるために使用しているメトリックスについては、IEEE Security & Privacy の「セキュリティ コード レビューを実行するためのプロセス」という記事で説明しています (この記事へのリンクは、私のブログの blogs.msdn.com/michael_howard/archive/2006/08/01/686029.aspx にあります)。

優先順位が最も高いのは、古いコードです。なぜなら、古いコードは新しいコードよりも、セキュリティ脆弱性を抱えている可能性がはるかに高いからです。脅威は常に進化しています。古いコード (ほんの数年前に構築したコードも含む) が構築された当時の脅威は、今日の脅威とは異なります。加えて、古いコードの構築に使用されたテクニックには、最新の防御テクニックやベスト プラクティスが欠落しています。同様に、レガシ コードは安全性の低い旧式のライブラリを使用して構築されています。さらに、古いコードは、ほとんどの開発者がセキュリティの専門知識をほとんど持ち合わせていなかった時代に、乏しい状況認識で構築されています。

私からのアドバイスは単純です。それは、すべての古いコードを手作業でレビューし、セキュリティ脆弱性を確認するということです。実際、これはマイクロソフトにおける SDL のセキュリティ プッシュ フェーズの目的でもあります。SDL の大きな目標は、開発者が新たな脆弱性を製品に加える可能性を軽減することですが、セキュリティ プッシュは、古いコードに含まれている問題を探すよう開発チームに要求します。

コードの複雑性、行数、コード チャーンなど、コード レビューの優先順位を決めるのに役立つメトリックはほかにもあります。しかし、潜在的な脆弱性の密集度を最もよく表すのは、コードの年齢です。

得られた教訓 すべてのソース コード ファイルを識別し、年齢 (コードが "生まれた" 日付) でランク付けします。最も古いコードから順に、静的分析を実行し、コードを手動でレビューします。

機能の廃止、削除、根絶

ある機能が、現在の脅威の情勢において十分なセキュリティを備えていない場合があります。数年前には安全だった機能も、既にそうではなくなっています。それはコードの脆弱性ではなく、今日のコンピュータ環境における変化によるものです。

このような機能の典型的な例として、印刷の状態を表示したり、別のユーザーの画面に表示される簡単なメッセージを送信したりする、Windows® の Alerter サービスがあります。このサービスは早い時期からスパム メカニズムとして利用されるようになったため、私たちは不本意ながらも Windows XP SP2 で Alerter を既定で無効にし、Windows Vista® では完全に取り除きました。

もう 1 つの例として、旧式の IPX プロトコルと SPX プロトコルがあります (IPv4 も旧式ですが、こちらはまた別の問題を抱えています)。Windows Vista では、コードが古く、ほとんどのユーザーは使用していないという理由により、Microsoft® Client for NetWare Networks のサポートを削除しました。

今後も、マイクロソフトは古い機能を慎重に取り除いていく予定です。これらの機能に依存しているユーザーもいるため、リスクと利便性のバランスをとることが重要になります。

得られた教訓 長年にわたりセキュリティ上の懸念になっていた古い機能を識別し、それらの機能を廃止するための計画を立てます。最初の段階では、そうした機能を引き続き提供しつつ、既定で無効にします。その後、製品の次のバージョンで、機能を完全に取り除きます。ただし、その機能がどうしても必要なユーザーのために、Web からダウンロードできるようにします。最後に、機能のサポートを停止します。重要なのは、常に顧客に情報を提供することです。

ツールの適切な利用

これまで私は、ツールに対して批判的でした。正確には、ツール自体を批判していたのではなく、一部の開発者がツールに過剰に依存していることを批判していました。私が言うツールとは、セキュリティ上の脆弱性を特定的に見つけ出すのに役立つ、静的コード分析やバイナリ分析を指します。しかし歳をとるにつれ、ツールに対する私の姿勢は多少和らいできました。

コードの量が非常に多い場合 (たとえば、100 万行以上)、そのすべてを手動でレビューするのはきわめて困難です。ツールを使用すると、膨大な量のコードをすばやく分析できます。しかし、ツールは人間の知性に代わるものではありません。多くのツールは、誤検知の割合を可能な限り低くするように設計されているため、脆弱性を見逃しがちです。また、セキュリティ分析ツールの多くは大量のエラーと警告を生成するため、どのバグが本物で、どのバグがノイズであるかを判断するのがきわめて困難な場合があります。

もちろん、実際に多数の問題が見つかる場合にツールの出力を単純に無視するわけにはいきません。セキュリティ脆弱性の根本的な原因を分析する場合、私たちは常に、なぜその問題がツールによって発見されなかったのかを分析します。考えられる理由は 3 つあります。つまり、ツールが脆弱性を検出しなかったか、検出したけれども優先順位は低いと誤って判断したか、ツールは脆弱性を検出したのに人間が優先順位の判断を誤ったかのいずれかです。この分析により、ツールと教育を徐々に手直ししていくことが可能になります。

分析ツールは、コード内にセキュリティ脆弱性が存在する可能性を判断するためにも非常に役立ちます。たとえば、2 つの製品があり、それぞれが約 100,000 行の C++ コードで構成されているとします。それぞれのコード ベースに対して、ツールを実行します。この例では、/W4 および /analyze コンパイラ スイッチをツールとして使用すると仮定します。一方のコード ベースでは、121 の /W4 警告と 19 の /analyze 警告が生成され、もう一方のコード ベースでは、235 の /W4 警告と 65 の /analyze 警告が生成されました。この場合、どちらのコードをより慎重にレビューする必要があるでしょうか。

最後になりますが、ツールはチェックイン前の新しいコードや修正済みコードに対して実行したときに、すばらしい効果を発揮します。これは、ツールがコードの監視係として機能し、いくつかの種類のバグを早い段階で検出するからです。

得られた教訓 分析ツールは、コードに対してどの程度の配慮が必要であるかを判断するのに役立ちます。また、分析の出力を使用して、コード全体の危険性を判断することもできます。ツールをチェックイン時に使用すると、バグを早い段階でキャッチできます。さらに、ツールを頻繁に実行することで、新しい問題にすばやく対処することが可能になります。ツールを数か月に 1 回程度しか実行しないと、一度に膨大な量の警告に対処しなければならなくなります。

自動化

セキュリティ向上のための取り組みの初期段階には、手動のコード レビューや手動のデザイン レビューなど、膨大な量の手動作業があります。作業を効率的に行うには、可能な限り多くのプロセスを自動化する必要があります。

私たちのチームでは、多数のオーダーメイド ツールを構築し、それらのツールから情報を収集する内部 Web サイトを作成して、中央のセキュリティ チームがツールからの出力をレビューできるようにしました。SDL の改善を提案するときは、改善を自動化する方法が提案に含まれている必要があります。自動化を推進する主な要因は、スケーラビリティと常時使用です。コードが大量に存在する場合は、自動化を行う必要があります。また、プロセスの一部を常に繰り返す必要がある場合も、自動化を検討することになります。

得られた教訓 プロセスを可能な限り自動化します。コードをスキャンし、結果を中央のサイトにアップロードしてセキュリティの専門家が分析できるようにするツールを構築または購入します。

セキュリティ脆弱性をゼロにすることはできない

残念ながら、セキュリティ脆弱性をゼロにすることはできません。Windows Vista の最初のセキュリティ アップデートを発行したときのことです。一部のユーザーは、マイクロソフトは Windows Vista でセキュリティの問題を解決したと主張していたはずだと、驚きの声を上げました。第 1 に、私の知る限り、そのような主張をした人はいません。そして第 2 に、セキュリティ脆弱性をゼロにすることは単純に不可能です。

もちろん、セキュリティ脆弱性をゼロにできればなによりですが、そうした状態を達成できると考えるのは間違いです。テクノロジの情勢は常に流動的であり、脅威は変化し続け、セキュリティ リサーチは継続的に行われています。前に述べたように、セキュリティは軍備拡大競争です。私たちが製品に防御の機能を加えると、攻撃者たちはそれに適応していきます。

現時点でコードに脆弱性がまったく見当たらなくても、新たな種類の脆弱性が発見されれば、状況は明日にも一変します。たとえば、2003 年 10 月 15 日、マイクロソフトは、Microsoft Exchange 5.5 に含まれている Outlook® Web Access の、クロス サイト スクリプティング (XSS) に対する脆弱性を解決するセキュリティ情報を発行しました。翌年の 3 月 4 日、Sanctum (後に Watchfire に買収され、さらに IBM に買収されました) は、HTTP レスポンス スプリッティングと呼ばれる、XSS に似た新たな脆弱性について概説する文書を発表しました。6 ヵ月後、マイクロソフトは HTTP レスポンス スプリッティングの脆弱性を解決するために、Micorosoft Exchange 5.5 に含まれている Outlook Web Access 用の新たなセキュリティ アップデートを発行しました。これは何を意味するのでしょうか。簡単に言うと、最初のセキュリティ情報を発行した時点では、レスポンス スプリッティングの問題は認識されていませんでしたが、その後状況が変化したということです。

得られた教訓 目標はセキュリティ脆弱性を減少させることであり、新たなテクニックや脆弱性を模索する攻撃者がいる限り、セキュリティの問題をゼロにすることは不可能であるということを、組織のメンバーに理解させる必要があります。

セキュリティは終わりのない戦いである

「セキュリティは万全だ、問題はもう解決した」と言える瞬間が訪れることはありません。これは前に指摘したポイントに似ていますが、ニュアンスは若干異なります。エンジニアのトレーニングを継続して行うことは、きわめて重要です。これを怠ると、技術が陳腐化し、危機感が徐々に低下する可能性があります。セキュリティは必要不可欠であり、システムの防御は最重要課題です。前のポイントで述べたように、新たな脅威や脆弱性は次々に発見されています。したがってセキュリティは終わりのない作業であるということを常に心得ておく必要があります。

また、セキュリティはもはや特別なものではないということも認識する必要があります。セキュリティは成すべき仕事の一部なのです。

得られた教訓 エンジニアのトレーニングを継続し、セキュリティの問題に対処することの重要性を常に認識するよう教育します。

セキュリティの特効薬はない

「SDL の最も重要な要素は何ですか」という質問をたびたび受けます。答えは、「すべて」です。仮に不要な部分があれば、その部分は SDL から削除されていたでしょう。SDL のすべての部分は、セキュリティ脆弱性の減少に貢献します。したがって、静的分析ツールを実行する、教育を徹底するなどの単一の作業でセキュリティ対策は万全だと主張する人がいたら、その人はすべてのセキュリティ問題に対処していないことになります。確かに、静的分析ツールは有効ですし、ユーザーへの教育も不可欠ですが、それだけでは不十分です。セキュリティは徹底して行う必要があり、またプロセスの一部である必要があります。

得られた教訓 セキュリティにはあらゆる側面から対処する必要があります。それができていない場合は、プロセスを変更する必要があります。

「目玉の数」の名言は正しい

著名なオープン ソース擁護者である Eric Raymond は、「目玉の数さえ十分あれば、どんなバグも深刻ではない」と述べています。彼の言うとおりです。しかし、この簡潔な名言には、重要な部分が抜けています。それは、「ただし、目玉にインセンティブと教育が与えられていれば」という条件です。

マイクロソフトでは、プロセス改善を採用するよう従業員に要求できます。なぜなら、これらの要求は仕事の一部だからです。これがインセンティブです。教育については、ライブ トレーニング、ラボ、オンライン トレーニングなど、さまざまな形式で提供しています。したがって、エンジニアがセキュリティ分野の最前で起こっていることに順応するための教材は豊富にあります。

得られた教訓 コードを確認する目玉が多いに越したことはありませんが、それには一定の枠組みが必要です。コード レビューを行う開発者に、現在のプロセスに順応するためのインセンティブと、最新のセキュリティ脅威に関する教育を提供する必要があります。

今日のサービス拒否は明日の悪用

マイクロソフトを含む、多くのソフトウェア ベンダは、この教訓を高い授業料を払って学びました。エンジニアは、コードの問題を確認し、「単なる DoS」だという理由で、問題を無視してしまう可能性があります。セキュリティ リサーチの世界は常に進化しており、特定の脆弱性に関する想定は一夜にして変わってしまう可能性があるということを忘れてはなりません。

得られた教訓 サービス拒否の問題を簡単に無視してはいけません。悪意のあるユーザーは簡単な作業で DoS の脆弱性を本格的なコード実行の脆弱性に変えることができます。

最後に

私が過去 5 年間に学んだことが 1 つあるとすれば、それはセキュリティにおいては、自分の考え方や見方を常に疑うことを覚悟し、最新の問題に後れを取らずに積極的に把握することが必要だということです。この記事で学んだ教訓を生かせば、適切な対応ができるはずです。

Michael Howard は、マイクロソフトのプリンシパル セキュリティ プログラム マネージャとして、セキュリティを向上させるためのプロセスの改善とベスト プラクティスに取り組んでいます。また、『Writing Secure Code for Windows Vista』、『The Security Development Lifecycle』、『Writing Secure Code』、『19 Deadly Sins of Software Security』など、セキュリティに関する多数の書籍 (すべて英語) を共同執筆しています。

© 2007 Microsoft Corporation and CMP Media, LLC. All rights reserved; 許可なしに一部または全体を複製することは禁止されています.


速習講座: クラッシュを分析してアプリケーションにおけるセキュリティの脆弱性を検出する

速習講座

クラッシュを分析してアプリケーションにおけるセキュリティの脆弱性を検出する

Adel Abouchaev and Damien Hasse and Scott Lambert and Greg Wroblewski
この記事の内容 : :
  • プログラム クラッシュの原因
  • 悪用可能なエラーとそうでないエラー
  • 分析のベスト プラクティス
  • エラーのトリアージに関する方針
この記事は次のテクノロジを使用しています:
Windows デバッグ、C/C++
コンテンツ
アクセス違反
データ書き込み時のアクセス違反
悪用不可能な例外
/GS 例外
NX 例外
実践する

プログラムでのクラッシュを確実に悪用不可能にするにはどうすればいいでしょうか。答えは簡単です。すべてのクラッシュを悪用可能と想定して、とにかく修正することです。少なくともこれは品質にかかわることであり、また製品を顧客に出荷する前に問題を修正した方が、多くの場合コストが低くて現実的です。悪用の可能性を判断するために必要な分析を実施すると、ある程度のコストがかかる場合があります。

セキュリティ上の悪影響を把握するためにメモリ破損に関連するプログラム エラーを分析する作業は、複雑になりがちで、間違いが生じることも少なくありません。メモリでのバッファの位置、考えられる上書きの対象、上書きのサイズ、上書きで使用される可能性のあるデータの制限事項、ランタイム実行環境の状態、設定されている防止メカニズムを回避する機能など、複数の要因を考慮する必要があります。簡単に言うと、これらの質問に完全に回答するには、エラーの根本原因を解明する必要があります。

すべてのエラーが目に見える形で明らかになるわけではありません。その一例が、Microsoft Security Bulletin MS07-017 で説明されている GDI のリモート コード実行の問題です。脆弱な解析コードを呼び出すソフトウェアが、生成され得るほぼすべての例外を例外ハンドラで回復し、何も起こらなかったかのように操作を続行したのです (詳細については、blogs.msdn.com/sdl/archive/2007/04/26/lessons-learned-from-the-animated-cursor-security-bug.aspx をご覧ください)。あまり知られていないもう 1 つの例は、スタック メモリとヒープ メモリである種の破損が発生した場合です。この場合、エラーが発生してもプログラムとその実行環境の現在の状態に明らかな兆候がまったく示されない可能性があります。

この記事では、メモリの破損によって任意のコード実行または少なくともサービス拒否が可能となるケースのように、セキュリティへの影響に関してプログラム クラッシュを分析する方法についてのガイダンスを示します。一般的なハードウェア例外やソフトウェア例外を挙げながら、これらの問題について考察していきます。また、このような調査を進める際に利用できる全体的なガイドラインも示します。たとえば図 1 では、調査プロセスの流れを図で示して、特定のクラッシュが悪用可能かどうかを判断できるようにしています。これらはガイドラインに過ぎません。クラッシュが悪用可能でないことを正しく診断するには、完全な根本原因分析が必要であることを忘れないでください。新たな手法や既存の攻撃手法の変種が絶えず発見されています。

図 1 アクセス違反分析の経路
図 1 アクセス違反分析の経路(画像を縮小するには、ここをクリックします)
図 1 アクセス違反分析の経路
図 1 アクセス違反分析の経路 (画像を拡大するには、ここをクリックします)

クラッシュの最大の原因は、ハードウェアまたはソフトウェアの例外です。代表的な最新のプロセッサは、さまざまなハードウェア例外を生成する可能性がありますが、Windows® 環境でソフトウェア セキュリティに関連する問題を引き起こすものはそのうちの一部に過ぎません。最も一般的なハードウェア例外はアクセス違反です。ここからは、まずハードウェア例外を分析する方法について説明し、その後にソフトウェア例外を取り上げます。

アクセス違反

アクセス違反例外 (0xc0000005= STATUS_ACCESS_VIOLATION) は、最新のプロセッサで、命令またはプログラム実行によるメモリ アクセスが、プロセッサ アーキテクチャまたはメモリ管理ユニットの構造で定義されている一定の条件を満たしていない場合に生成されます。

単純なクラッシュによって発生する可能性があるのはサービス拒否だけですが、クラッシュをコード実行などの危険な効果の実現に利用できないと想定するのは危険です。クラッシュを分析する際には、軽微な例外でもメモリ本体全体が潜在的な攻撃者の制御下に置かれるという前提で、アクセス違反のほとんどが攻撃者によるデータ制御につながる可能性があると考える必要があります。このことは、命令によるデータの読み取りまたは書き込みで発生した例外に当てはまります。

アクセス違反が攻撃者によるデータ制御につながる可能性がある場合は、メモリからの読み取りでアクセス違反が発生するたびに、攻撃者の制御するデータが読み込まれます。このような操作がセキュリティに与える影響を特定することは容易ではありません。バイナリまたはソース コードで完全なデータ フロー分析を実施すれば、ソース アドレス コントロールの範囲、および特定の実行時点でプログラムにランダムなデータを与えた結果を確認できます。これは時間のかかる困難な作業です。そこで私たちは、読み取りアクセス違反によるクラッシュでのコード実行の可能性を迅速に分析する簡単なヒューリスティックを開発しました。

次の例は、レジスタ eax 内の無効なメモリ ポインタが原因でクラッシュが発生したことを示しています。この場合、メモリの内容を制御することで、攻撃者はプログラム フローを完全に制御できるようになります。

Application!Function+0x133:
3036a384 eb32            call     [eax]           ds:0023:6c7d890d=??
0:000> ub
mov    eax, [ebx]  ->  eax = invalid memory pointer
...                         (instructions not affecting register eax)
call        [eax]  ->  crash

読み取られるアドレスを攻撃者が十分に制御できなければ、サービス拒否状態と見なすことができます。たとえば、一般的な Windows ユーザー モード環境では、初期化された NULL ポインタで発生し、攻撃者の影響を受けることがないクラッシュが、コード実行につながることはありません。

次の例では、レジスタ eax の値を通じてアドレス ゼロを参照したためにクラッシュが発生したことがわかります。

Application!Function+0x133:
3036a384 8b14            mov     ecx, [eax]           ds:0023:00000000=??
0:000> ub
xor    eax, eax
...                         (instructions not affecting register eax)
cmp    ebx, 2
jne        label123
mov    ecx, [eax]  ->  crash, eax = 0 (NULL)

逆アセンブルすることで (Visual Studio® コマンド ライン デバッガで ub コマンドを使用)、このレジスタでのデータ フローを追跡し、レジスタの値が悪意のある入力の影響を受けないことを確認できます。実際に、この例ではレジスタが自身を XOR することでゼロに設定され、クラッシュした命令に到達するまで使用されていませんでした。

クラッシュした命令から、不具合が悪用される可能性が直ちに判明するとは限りません。たとえば、次の命令を逆アセンブルすると、エラーが発生した命令 (前の段落で説明) の後にクリティカルな命令が続くという制御フローが不具合の原因であることがわかります。

(1258.1638): Access violation - code c0000005 (second chance)

Application!Function+0x123:
3036a384 8b12            mov    eax, [ebx]           ds:0023:6c7d890d=??
0:000> u
mov    eax, [ebx]  -> crash, ebx points to invalid memory
...             (instructions not affecting regist an example er eax)
call        [eax]  ->  possibility of code execution

この例は最初の例に似ていますが、今回はレジスタ eax にデータを読み込む命令でクラッシュが発生しています。この操作自体にセキュリティ上の問題はありませんが、逆アセンブルから明らかになるのは、レジスタ ebx の値の制御がレジスタ eax の値の制御に影響し、それがコード実行につながる可能性があるということです。

残りのケースは、プログラム実行中に悪質なデータ挿入をシミュレートすることで分析できます。これはデバッガから手動で行うか、またはデバッグ ツールを通じて自動的に行います。アクセス違反の原因となる命令に到達したら、有効なメモリ アドレスを指すようにソース アドレスを変更してもう一度命令を実行するか、または宛先レジスタにランダムなデータを入れて問題のある命令をスキップし、実行を継続します。このプロセスを検出したアクセス違反ごとに繰り返し、悪用可能な状態を見つけます。

それでは 2 つの例を分析してみましょう。まず、ソース アドレスを変更するときのデータ フローを追跡します。

Application!Function+0xa70:
3036a37e 8b4708          mov     eax,dword ptr [edi+8] ds:0023:040fd004=????????

ここでのクラッシュは、レジスタ edi 内の不適切な値が原因です。この値を変更して有効なメモリ領域を指すようにします。選択肢は多数ありますが、実際にはレジスタ eip の現在値を使用する場合がほとんどです。これにより、edi の新しい値の周囲にある比較的まとまった大きさのメモリ領域が有効となります。また、コードを含んでいるメモリ ブロックが常に読み取り専用とマークされるので、そのメモリ ブロックへの後続の書き込みを捕捉できるようになります。

0:000> r edi=eip
0:000> g

レジスタ edi をレジスタ eip の現在値に設定して実行を続けると、レジスタ esi で別の例外が発生するので、そこで同じプロセスを繰り返します。

(1258.1638): Access violation - code c0000005 (second chance)

Application!Function+0xa76:
3036a384 f60601          test    byte ptr [esi],1           ds:0023:6c7d890d=??
0:000> r esi=eip
0:000> g

さらに実行を続けると、コード セグメントにデータを書き込もうとする命令で、書き込みアクセス違反例外が発生します。これは、攻撃者も任意のメモリ アドレスにデータを書き込めることを意味します。

 (1258.1638): Access violation - code c0000005 (second chance)

Application!Function+0xbef:
3036a4fd 894710          mov     dword ptr [edi+10h],eax ds:0023:3036a38e=0c46f60d

ご覧のように、初期の読み取りアクセス違反が、コード実行の可能性がある本当のセキュリティ問題であることが判明しました。

次に、宛先データを設定して命令をスキップする方法で、データ フローを追跡してみます。前回と同じクラッシュを分析しながら、宛先データを繰り返し変更します。最初の例外が発生したら、レジスタ eax を追跡しやすい任意の値に設定し、レジスタ eip を現在の命令のサイズ分だけ増やしてその命令をスキップします。

Application!Function+0xa70:
3036a37e 8b4708          mov     eax,dword ptr [edi+8] ds:0023:03f93004=????????
0:000> r eax=deadbeef
0:000> r eip=eip+3
0:000> g

実行を続けると次の例外が発生します。これには宛先がないため、そのまま命令をスキップできます。

(1258.1638): Access violation - code c0000005 (second chance)

Application!Function+0xa76:
3036a384 f60601          test    byte ptr [esi],1           ds:0023:deadbefb=??
0:000> r eip=eip+3
0:000> g

さらに実行を続けると、同じ書き込みアクセス違反例外に到達します。つまり、悪用可能な問題である可能性を示しています。

(1258.1638): Access violation - code c0000005 (second chance)

Application!Function+0xbef:
3036a4fd 894710          mov     dword ptr [edi+10h],eax ds:0023:03f9300c=0c46f60d

悪質なデータ挿入をシミュレートする 2 つの方法は、たいていどちらも同じ結果をもたらしますが、これまでの経験から、両方法の対象範囲であるコード パスは異なる場合が多く、問題が悪用される可能性を示すのは一方の方法だけであると言えます。ここまでに説明した方法では、いずれも潜在的に悪用可能な読み取りアクセス違反によるクラッシュをきわめて迅速に検出できますが、特定のクラッシュが悪用可能な問題ではないことを最終的に検証できないことは忘れないでください。

データ書き込み時のアクセス違反
クラッシュ分析に関するリソース
  • MSDN Security Developer Center
  • Microsoft セキュリティ情報
  • Visual Studio デバッガのセキュリティ
  • Windows 用デバッグ ツール

データ書き込み中のアクセス違反は、メモリ破損の可能性を示し、ほぼ必ずコード実行の可能性を伴う悪用可能な状態につながります。多くの場合、このような書き込みは、クラッシュしたプログラムにバッファ オーバーフローの条件が存在することを示します。実際に、書き込みアクセス違反によるクラッシュで悪用可能と見られないものはごくわずかです。それでも、このような状況で問題の根本原因を解明するには、完全なデータ フロー分析を実施する必要があります。破損が要因となって攻撃者がデータを上書きして実行フローに任意の影響を及ぼす事態にならないことを確認する必要があります。

データ書き込みによるアクセス違反のほとんどは、悪質なコード実行を引き起こす悪用可能なケースにつながる可能性があります。一般に、コード実行はスタックまたはヒープ上の (任意の) 断片的なメモリを上書きすることで行われます。

次の例では、宛先レジスタがスタックの上限に到達し、割り当てられていないメモリ領域に達したときにメモリ コピー命令でクラッシュが発生しています。コピー操作のサイズはアドレス [ebp-8] にある変数から取得されており、クラッシュはそのアドレスが攻撃者の制御下にあることを示唆しています。

Application!Function+0x143:
3036a384 f3a4            rep     movsb           es:0023:00140000=??   ds:0023:0125432c=41414141
0:000> ub
mov    esi, [ebp-4]
mov    edi, [ebp+4]
mov    ecx, [ebp-8]
rep    movsb  ->  write access violation if value in ecx is big enough

書き込みアクセス違反がコード実行につながらないのは、攻撃者によって制御される宛先アドレスが無効なメモリを指しているか、またはプログラムの実行に影響しないデータを指している場合に限られることは間違いありません。実際に、この条件を満たす書き込みアクセス違反はごくわずかです。Windows ユーザー モード環境では、NULL ポインタまたはシステム アドレス空間 (通常は 2 GB より上のアドレス) への書き込みが、悪用不可能な問題として挙げられます (アドレス 0x00000000 のページを割り当てられないことが前提です)。また、サーバーでは、命令がサービス拒否につながるため、命令が管理者以外のユーザーによって実行される場合は、セキュリティ上の脆弱性と見なす必要があります。

この例は、読み取りアクセス違反に関して取り上げた前の例と似ています。速やかに逆アセンブルすることで、レジスタ eax が値ゼロに初期化され、クラッシュした命令に到達するまで変更されていないことがわかります。

Application!Function+0x133:
3036a384 8d14            mov     [eax], ecx           ds:0023:00000000=??
0:000> ub
xor    eax, eax
...                       (instructions not affecting register eax)
cmp    ebx, 2
jne    label123
mov    [eax], ecx  ->  crash, eax = 0 (NULL)

悪用不可能な例外

カーネル モードではなくユーザー モードでは、この種類の例外が悪用の 1 つの段階に使用されることはまずありません。通常このような例外はサービス拒否に終わります。場合によっては、別の脆弱性が原因で例外ハンドラが変更されると、このような例外が悪質なコード実行につながることもあります。サービス拒否は、サーバー プラットフォームでは優先度の高いバグであり、ワークステーションでは中位の優先度のバグです。

このような例外の一例が、静的/グローバルな逆参照 (読み取りと書き込み) です。次の例のプロセスは、ページ 0x310000 への読み取りアクセス権がないために読み取りアクセス違反を引き起こすか、または同ページへの書き込みアクセス権がないために書き込みアクセス違反を引き起こします。

mov ebp, 310046h
mov eax, [ebp+4h]

inc eax
mov [ebp+8h], eax

最初の 2 つの例外は慎重に分析する必要があります。静的/グローバルな値が指しているメモリ内のアドレスで、悪質なコードが長さを制限されずに書き込みを行い、また他の構造を上書きして複雑な悪用を段階的に行う可能性がある場合は、このケースを悪用可能とマークする必要があります。どの制御構造にも含まれないアドレスに単一の DWORD を格納することのみが許可されている場合、悪用される可能性はほとんどありません。ただし、実際にプログラムを実行し、値を監視して、このことを検証する必要があります (ランタイム分析とも呼ばれます)。格納された値が後からコードで使用されなければ、別の悪用可能な状態を招くことはないので、無視してかまいません。この値がメモリ アドレスとして、またはメモリ コピー操作で使用される可能性がある場合は、悪用のリスクが高まります。

もう 1 つの事例として、制御不可能なアドレス空間 (ページ 0 のようなケース) 内の静的/グローバル アドレスでのコード実行が挙げられます。ここでは、オペランドのアドレスにあるメモリ ページが書き込みに関して制御可能かどうかで、悪用の可能性が決まります。

0040137F B8 DE C0 AD DE    mov     eax, 0DEADC0DEh
00401384 FF D0             call    eax

静的/グローバル アドレスでのコード実行の分析からさらに読み取れることがあります。通常のプロセス実行には関連しないページ (ページ 0 またはアドレスが 0x80000000 より上のページ) にアドレスが属していれば、悪用は不可能です。

ゼロによる除算も例外の原因となりますが、この時点で直接悪用することはできません。この状況を追加で分析して、この例外によって CPU が悪用につながるコードを実行する状態になるかどうかを判断する必要があります。

004013D6 33 C9    xor    ecx, ecx
004013D8 8B C1    mov    eax, ecx
004013DA 40       inc    eax
004013DB F7 F1    div    ecx

ハンドルされない C++ 例外は、プロセスの実行を中断する可能性があります。実行時にこの例外が発生すると、アプリケーションが強制終了されます。デバッガを使用している場合は、ハンドルされない例外が発生した後も実行を継続できます。C++ 例外は、ランタイム ライブラリの例外処理関数が呼び出されると発生します。

00401902    mov     [ebp+var_4], 1
00401909    push    offset __TI1H
0040190E    lea     eax, [ebp+var_4]
00401911    push    eax
00401912    call    __CxxThrowException@8 ; _CxxThrowException(x,x)

この状態が悪用可能となるのは、例外ハンドラ メカニズムが既に上書きされている場合に限られます。それ以外の場合は、ハンドルされない C++ 例外を悪用できません。この例外のスタック トレースを図 2 に示します。

Figure 2 ハンドルされない C++ エラーのスタック トレース (Close Figure 2)
CommandLine: test.exe
Symbol search path is: srv*c:\Symbols*\\symbols\symbols
Executable search path is:
ModLoad: 00400000 00405000   test.exe
ModLoad: 7c900000 7c9b0000   ntdll.dll
ModLoad: 7c800000 7c8f5000   C:\WINDOWS\system32\kernel32.dll
ModLoad: 78130000 781cb000   C:\WINDOWS\WinSxS\x86_Microsoft.VC80.CRT_1fc8b3b9a1e18e3b_8.0.50727.762_x-ww_6b128700\MSVCR
80.dll
ModLoad: 77c10000 77c68000   C:\WINDOWS\system32\msvcrt.dll
(1494.14c4): Break instruction exception - code 80000003 (first chance)
eax=00251eb4 ebx=7ffda000 ecx=00000004 edx=00000010 esi=00251f48 edi=00251eb4
eip=7c901230 esp=0012fb20 ebp=0012fc94 iopl=0    nv up ei pl nz na po nc
cs=001b  ss=0023  ds=0023  es=0023  fs=003b  gs=0000             efl=00000202
ntdll!DbgBreakPoint:
7c901230 cc              int     3
0:000> g
1
(1494.14c4): C++ EH exception - code e06d7363 (first chance)
(1494.14c4): C++ EH exception - code e06d7363 (!!! second chance !!!)
eax=0012fee0 ebx=00000000 ecx=00000000 edx=781c3c58 esi=0012ff68 edi=004033a4
eip=7c812a5b esp=0012fedc ebp=0012ff30 iopl=0    nv up ei pl nz na pe nc
cs=001b  ss=0023  ds=0023  es=0023  fs=003b  gs=0000             efl=00000206
kernel32!RaiseException+0x53:
7c812a5b 5e              pop     esi
0:000> kb
ChildEBP RetAddr  Args to Child
0012ff30 78158e69 e06d7363 00000001 00000003 kernel32!RaiseException+0x53
0012ff68 00401917 0012ff78 004022b8 00000001 MSVCR80!_CxxThrowException+0x46
0012ff7c 004011b2 00000001 00353098 003530d0 test!wmain+0x27
0012ffc0 7c816fd7 00011970 7c9118f1 7ffda000 test!__tmainCRTStartup+0x10f
0012fff0 00000000 004012fb 00000000 78746341 kernel32!BaseProcessStart+0x23

スタック オーバーフロー例外には注意が必要です。この例外は、実行時の他の問題を隠したり、コード フローの流れを変えたりする場合があります。また、コンパイラが生成した /GS などの保護メカニズムを無効にしたり、使用中のメモリを解放してアプリケーションの整合性を乱したりすることもあります。スタック スペースがほぼ使い果たされると、C++ メソッドの内部でスタック オーバーフロー例外によりアプリケーションがエラーとなり、重要なコードが正常に実行されなくなる可能性があります。このような状況では、追加のトリアージが必要になります。

/GS 例外

/GS (0xc0000409=STATUS_STACK_BUFFER_OVERRUN) 例外は、リターン アドレスを保護するセキュリティ Cookie の改ざんが Windows で検出されるとスローされます。/GS の目標は、コード実行につながるバッファ オーバーランをサービス拒否攻撃に変えることであるため、このようなクラッシュが検出された場合は確実にセキュリティ バグが存在します (ただし、メモリのバグ、マザーボードのクロックアップ、ハードウェアの不具合などの問題が原因で、Cookie を検証するコードが、実際にはバッファ オーバーランを起こすことなく中断される場合があります)。

Windows Vista® では、STATUS_STACK_BUFFER_OVERRUN が検出されると、int 3 例外がスローされます (デバッガの介在が前提)。以前のバージョンの Windows では、セキュリティ Cookie が改ざんされたかどうかを検出するために、kernel32!UnhandledExceptionFilter にブレークポイントを設定する必要があります (そうしないと、プロセスが終了してユーザーへの通知が行われません)。

図 3 では、関数 foo がスタック バッファに過剰にデータをコピーすることでバッファがオーバーランし、それが原因で /GS Cookie が上書きされます。

Figure 3 /GS Cookie の上書き (Close Figure 3)
<the /GS cookie is being setup in the function prolog>
0:000:x86> u gs!foo
gs!foo:
010011b9 8bff            mov     edi,edi
010011bb 55              push    ebp
010011bc 8bec            mov     ebp,esp
010011be 83ec18          sub     esp,18h
<global cookie will be moved to eax register>
010011c1 a100200001      mov     eax,dword ptr [gs!__security_cookie (01002000)]
010011c6 53              push    ebx
010011c7 56              push    esi
010011c8 57              push    edi
010011c9 8b7d08          mov     edi,dword ptr [ebp+8]
<cookie will be placed on the stack (ebp-4)>
010011cc 8945fc          mov     dword ptr [ebp-4],eax
<content of the source (src) and destination buffers (dst) – before the overrun>
0:000:x86> dv /V
000bfefc @ebp+0x08             src = 0x009d16cb "123456789012345678901234567890"
000bfedc @ebp-0x18             dst = char [20] ""
<value of the security cookie on the stack; note that it is located right after the buffer, before saved ebp (0x000bff24) and the return address (0x0100124a)>
0:000:x86> dd 000bfedc+0n20 l3
000bfef0  0000b96f 000bff24 0100124a
<value of global cookie>
0:000:x86> dd gs!__security_cookie l1
01002000  0000b96f
... code runs ...
<after the overrun has happened, the cookie got overwritten (with 0x34333231) as well as the last two bytes of the return address (with (0x3039)>
0:000:x86> dd 000bfedc+0n20 l3
000bfef0  34333231 38373635 01003039
<in the function epilog before it returns the /GS cookie gets checked (i.e. the return address has not been used yet)>
0:000:x86> u gs!foo+0x54
gs!foo+0x54:
0100120d 59              pop     ecx
<cookie is placed in the ecx register>
0100120e 8b4dfc          mov     ecx,dword ptr [ebp-4]
01001211 5f              pop     edi
01001212 5e              pop     esi
01001213 5b              pop     ebx
01001214 e882020000      call    gs!__security_check_cookie (01002000)
01001219 c9              leave
0100121a c20400          ret     4
<the below functions compares the global cookie with the one that was stored in the stack (currently in ecx register)>
0:000:x86> r
eax=0000000c ebx=7efde000 ecx=34333231 edx=00000000 esi=00000000 edi=00000000
eip=0100149b esp=000bfed8 ebp=000bfef4 iopl=0         nv up ei pl zr na pe nc
cs=0023  ss=002b  ds=002b  es=002b  fs=0053  gs=002b             efl=00000246
gs!__security_check_cookie:
0100149b 3b0d00200001    cmp     ecx,dword ptr [gs!__security_cookie (01002000)] ds:002b:01002000=0000b96f
<if a debugger was attached to the process, then an (int 3) will be issued (in Windows Vista) before the process gets terminated>
0:000:x86>
STATUS_STACK_BUFFER_OVERRUN encountered
 (15d0.1708): Break instruction exception - code 80000003 (first chance)
ntdll!DbgBreakPoint:
773e0004 cc              int     3

NX 例外

NX (0xc0000005=STATUS_ACCESS_VIOLATION) 例外は、実行可能とマークされていないページ (つまり PAGE_EXECUTE、PAGE_EXECUTE_READ、またはその他の関連フラグが設定されていないページ) で実行されているコードが Windows で検出されるとスローされます。NX は、64 ビット版のオペレーティング システムで実行されます。アンパッカやデジタル著作権管理 (DRM) などのアプリケーションは、ヒープ上でコードを実行するため、すべての NX 例外をセキュリティ上の脆弱性と見なすことはできません。この場合も、バグの根本原因を解明してセキュリティへの影響がないことを確認する必要があります。

NX に限らず、この種の例外はエラー コード 0xc0000005 を使用することに注意してください。割り当てられていないメモリからの読み取りなどによって誤動作するアプリケーションは、このエラーをスローする可能性があります。例外/エラー コードが本当に NX に関連しているかを把握するには、ページに設定されている保護を確認する必要があります。ページが実行可能とマークされていない場合は、NX 例外が発生しています。それ以外の場合は、他の種類の問題が発生しています。たとえば、図 4 では初回の例外が発生していますが、命令は有効と思われ、また有効なデータを参照しています。

Figure 4 潜在的な NX 例外 (Close Figure 4)
(1424.a78): Access violation - code c0000005 (first chance)
First chance exceptions are reported before any exception handling.
This exception may be expected and handled.
00000000`003b0020 2801            sub     byte ptr [rcx],al ds:00000000`7702e6aa=c3
0:000> r
rax=0000000000000001 rbx=000000000021fd10 rcx=000000007702e6aa
rdx=0000000000000000 rsi=000000000000000a rdi=0000000000000000
rip=00000000003b0020 rsp=000000000021fca0 rbp=00000000ff130000
 r8=000000000021fc98  r9=00000000ff130000 r10=0000000000000000
r11=0000000000000244 r12=00000000ff131728 r13=0000000000000000
r14=0000000000000000 r15=0000000000000000
iopl=0         nv up ei pl zr na po nc
cs=0033  ss=002b  ds=002b  es=002b  fs=0053  gs=002b             efl=00010246
00000000`003b0020 2801            sub     byte ptr [rcx],al ds:00000000`7702e6aa=c3
<when we execute the actual instruction again, we hit a second chance access violation>
0:000> t
(1424.a78): Access violation - code c0000005 (!!! second chance !!!)
00000000`003b0020 2801            sub     byte ptr [rcx],al ds:00000000`7702e6aa=c3
<a closer look at the protection of the page, shows that it does not have PAGE_EXECUTE set, this explains why the code cannot be executed>
0:000> !address 00000000`003b0020
 ProcessParametrs 00000000003b25c0 in range 00000000003b0000 00000000003e0000
 Environment 00000000003b1370 in range 00000000003b0000 00000000003e0000
    00000000003b0000 : 00000000003b0000 - 0000000000030000
                    Type     00020000 MEM_PRIVATE
                    Protect  00000004 PAGE_READWRITE
                    State    00001000 MEM_COMMIT
                    Usage    RegionUsageHeap
                    Handle   00000000003b0000

実践する

ここまででおわかりのように、セキュリティ上の影響についてプログラム エラーを分析する作業は複雑であり、間違いが生じることもあります。この記事では、クラッシュを分析する際に遭遇する可能性のある一般的な例外として、読み取り/書き込みアクセス例外、ゼロによる除算、C++ 例外、/GS 例外、NX 関連の問題を取り上げました。忘れてはならない重要なポイントは、あるクラッシュが悪用可能かどうかを正しく診断するには、完全な根本原因分析が必要であるということです。

アプリケーションの分析に役立つガイドラインを示すために、その情報をワークステーションですぐに参照できるクイック リファレンスにまとめました。図 5 は、この記事で説明したガイドラインを表にしたものです。たとえば、CPU が書き込みアクセス権を持たないメモリ ページにデータを書き込もうとして書き込みアクセス違反が発生した場合は、その問題を修正する必要があります。

Figure 5 修正トリアージの基準 (Close Figure 5)

例外 | 解説
修正が必要 |
書き込みアクセス違反 | CPU が書き込みアクセス権のないメモリ ページにデータを書き込もうとするとアクセス違反が発生します。
命令ポインタでの読み取りアクセス違反 (EIP でのアクセス違反) | CPU が読み取りアクセス権のないメモリ ページで命令を実行しようとするとアクセス違反が発生します。
読み取りアクセス違反 | 次のいずれかが発生する場合があります。• カウント レジスタ (ecx) が大きい場合に、rep アセンブリ命令 (Intel プロセッサ上) でアクセス違反が発生します。• mov 命令で、結果が mov の直後の命令における呼び出しの宛先として使用される場合に、アクセス違反が発生します。• mov 命令で、結果が後から rep 命令でソース (esi)、宛先 (edi)、またはカウント (ecx) として使用される場合に、アクセス違反が発生します。
トリアージが必要 |
読み取りアクセス違反 | NULL (アドレス 0x00000000) からの読み取り、または入力によって制御されないメモリ アドレスからの読み取りでアクセス違反が発生し、かつ攻撃者が値を操作できない場合。
通常は悪用不可能 |
ゼロによる除算 | アクセス違反が単独の問題として発生し、かつこのアクセス違反の前に他の構造 (例外ハンドラなど) が破損していない場合。
C++ 例外 | 上記と同じ。

同様に、この記事で最初に紹介した図 1 は、特定のクラッシュが悪用可能かどうかを判断できるように、このプロセスを図式化して別の観点から示しています。一番上のノードから開始して、次に進むノードを判断します。悪用不可能または悪用可能のどちらかのノードに到達するまでこのプロセスを進めます。

ここで示したガイドラインをプログラム エラーのトリアージにお役立てください。詳細については、「クラッシュ分析に関するリソース」のリンクを参照してください。クラッシュがマイクロソフト製品で悪用可能であると診断された場合は、secure@microsoft.com までご報告ください。

Adel Abouchaev (CCIE#12037、MCSE、CISSP) は、マイクロソフトの Secure Windows Initiative (SWI) チームに在籍するセキュリティ ソフトウェア エンジニアです。ファジー化ツールとランタイム分析ツールを担当し、マイクロソフトの製品チームにセキュリティ分析およびセキュリティ評価を自動化する方法を提供するために尽力しています。

Damien Hasse は、マイクロソフトのリード セキュリティ ソフトウェア エンジニアとして、Microsoft Security Response Center (MSRC) で脆弱性とセキュリティ上の脅威を調査するセキュリティ調査員のチームを指揮しています。

Scott Lambert は、マイクロソフトの Secure Windows Initiative (SWI) チームに在籍するセキュリティ プログラム マネージャです。各種ファジー化ツールを始めとする社内セキュリティ ツールの強化を担当しています。これまでの業界での経験を活かし、SWI ツールが大半の種類の脆弱性を確実に見つけ出せるように努めています。

Greg Wroblewski は、マイクロソフトのセキュリティ レスポンス チームに在籍するセキュリティ ソフトウェア エンジニアです。


コード レビュー: アプリケーションの出荷前に脆弱性を見つけて修正する

コード レビュー

アプリケーションの出荷前に脆弱性を見つけて修正する

Michal Chmielewski and Neill Clift and Sergiusz Fonrobert and Tomasz Ostwald
この記事の内容 : :
  • コード レビューのプロセス
  • コード レビューの優先順位を決定する
  • セキュリティ上の脆弱性の種類
  • レビュー結果を利用する
コンテンツ
ソフトウェアのセキュリティ上の脆弱性
コード エラーを見つける
コード レビューのプロセス
レビュー作業の優先順位を決定する
コード レビューの戦術
脆弱性の種類
結果を処理する
まとめ

ソフトウェア開発では、小さなコード エラーから重大な脆弱性が生じて、システムやネットワーク全体のセキュリティが損なわれることもあります。しかし、セキュリティ上の脆弱性の原因は、多くの場合、1 つのエラーではなく、開発サイクルの過程で発生する一連のエラーにあります。つまり、コード エラーが導入され、テスト フェーズで見逃されて、用意されている防御機構で攻撃を防ぐことができない場合に、セキュリティ上の脆弱性が発生するのです。

セキュリティは、ソフトウェア開発のすべてのフェーズの優先課題であり、ソフトウェアの脆弱性の防止に向けた取り組み - リリース前に検出するための取り組みはもちろん、実際の影響を抑えるための取り組みも (製品の攻撃対象領域を減らすなど) - が必要です。マイクロソフトでは、そうしたセキュリティに対する全体的なアプローチが、セキュリティ開発ライフサイクル (SDL) を通じて実施されています。SDL は、開発者の教育、設計の改善、コーディング プラクティスやテスト プラクティスの利用、製品リリース後の緊急対応の準備など、ソフトウェア開発の主要なフェーズをすべてカバーしています (図 1 を参照)。SDL はコード レビューに対する唯一のアプローチではありませんが、ここで取り上げる内容のほとんどは SDL に基づいています。

図 1 セキュリティ開発ライフサイクル
図 1 セキュリティ開発ライフサイクル(画像を縮小するには、ここをクリックします)
図 1 セキュリティ開発ライフサイクル
図 1 セキュリティ開発ライフサイクル (画像を拡大するには、ここをクリックします)

この記事では、開発者やセキュリティの専門家によって行われる手動のセキュリティ コード レビューについて説明します。SDL によって定義されるプロセスでは、こうした作業は通常、セキュリティ プッシュや侵入テストの間に行われ、最終的なセキュリティ レビューに関連付けられます。コード エラーを見つけるにはさまざまな方法がありますが、手動のコード レビューは、高度なツールと比べても、精度と品質の面で価値があることがはっきりと証明されています。その反面、最も実施コストが高いのも事実です。

ここではそのほかに、大規模ソフトウェア プロジェクトのコンテキストにおけるセキュリティ コード レビューのメリットとデメリットについても詳しく説明します。この記事は、マイクロソフトが過去数年間にリリースした主な製品のレビューを通じて長い間に蓄積された経験に基づいています。

ソフトウェアのセキュリティ上の脆弱性

ある程度の規模と複雑さを持つソフトウェア製品は、セキュリティ上の脆弱性と無縁と見なすことはできません。あらゆる手段を尽くしてコード エラーを減らし、その実際の影響を和らげる必要がありますが、それでも、見逃されるものは必ず出てきます。セキュリティに影響するソフトウェア エラー (脆弱性) はアプリケーションのさまざまなレベルに存在し、開発サイクルのさまざまなフェーズで導入されます。

脆弱性はコードだけにとどまりません。早ければ要件定義の段階で、安全な方法では実装できない要件という形で導入されます。また、製品の基本設計に欠陥が含まれることもあります。たとえば、不適切なテクノロジが選択されていたり、セキュリティの観点から正しくない使われ方をしていたりする場合があります。こうした問題は、設計レビューや脅威のモデル化によって製品開発の早い段階ですべて特定されるのが理想です。

セキュリティ コード レビューの主な目的は、セキュリティ上の脆弱性の大部分の原因となるコード レベルの問題を見つけることです。この段階で設計上の問題が見つかることもありますが、そうした問題は、脅威のモデル化や開発プロセスのその他の側面の改善の必要性に関連している可能性があります。ソース コード レビューは、セキュリティ以外の問題を優先して行われる場合もありますが、ここでは、製品が行っているセキュリティの保証が破られたりシステムのセキュリティが損なわれたりする原因となるコードの脆弱性を見つけることを目標とします。

セキュリティの問題 (検証が行われないことによる問題など) を引き起こす可能性があるコード エラーがセキュリティ上の脆弱性となるのは、特定の条件が満たされた場合だけです。たとえば、攻撃の対象となるセキュリティの境界が必要なほか、攻撃者がある程度のレベルでデータや環境を制御できる必要があります。問題が存在するコードが、攻撃者と同じセキュリティ コンテキストで実行される場合は、脆弱性を悪用するために特権を取得される可能性はありません。また、脆弱性が存在しても、それを含むコードに攻撃者がアクセスできなければ、そのコードが実行されることはありません。こうしたコード エラーは、製品の信頼性に影響を与える可能性はありますが、実際の脆弱性と見なされるべきではありません。セキュリティ コード レビューの究極の目標は、攻撃者によってアクセスされ、セキュリティの境界を越えるために利用される可能性があるコードの脆弱性を見つけることです。

脆弱性にアクセスできることと、それを悪用できることの違いに注意してください。攻撃に成功されたとしても、/GS フラグ、/SafeSEH フラグ、ASLR (Address Space Layout Randomization) などのプラットフォーム強化機能によってそれを軽減することができます。

コードの脆弱性を悪用される可能性の調査は、コード レビューの調査の範囲には含まれません。これは主に、利用可能な軽減策が十分なものであるかどうかを証明するのは一般に不可能であるからです。コード レビューアの責任は、コードの脆弱性が存在し、適切なトリアージが行われていることを確認するところまでです。その時点を過ぎたらそのバグは、修正を要するまったく別の問題と見なされる必要があります。

セキュリティ コード レビューの主な目的はコードの脆弱性を特定することですが、その他の成果が得られる場合もあります。コードの全体的な品質、冗長性、実行されないコード、不必要な複雑さなどに関するフィードバックや、攻撃対象領域の削減、データの検証、コードのクリーンアップ、コードの読みやすさ (コメントの改良など) などを改善するためのアドバイスが、レビューアから提供される場合もあります。ただし、そのような成果を文書にまとめるのには時間がかかるため、そうしたアドバイスを結果に含めるかどうか、セキュリティの問題の特定に集中すべきかどうかを、開始前に決定しておく必要があります。

コード エラーを見つける

コード エラーを見つけるにはさまざまな方法があります。どの方法にもそれぞれ固有の利点と実用上の制限があるため、コード レビューとその他の選択肢との違いを理解しておくことが重要です。ここでは、記事の目的のために、ソース コードと設計文書の両方が利用可能で、製品のいわゆる "ホワイトボックス" 分析 (内部の分析。これに対して、"ブラックボックス" アプローチでは、外部から見える動作のみが対象になります) を行うものと仮定します。

コード レビューは、コード エラーを見つけるための手動の静的なアプローチと言えます。同様に、他の 2 つの一般的なアプローチは、自動化された静的なアプローチと、自動化された動的なアプローチと言い表すことができます。自動化された静的なアプローチは、パターンを使用して定義された既知の種類の問題を見つけるためにソース コードに対して実行される、静的コード分析ツールの形をとるのが一般的です。PREfix や PREfast はこのアプローチの例です。自動化された動的なアプローチの代表的な例は、ファイル、プロトコル、API を主な対象とする自動化コード テスト手法 (ファジー化など) です。これらのソリューションはブラックボックス アプローチにも適用できますが、ファイル形式などの内部要素に関する情報が利用可能で、それらを適切に使用した場合に、はるかに良い結果が得られます。

これらのアプローチは、それぞれ実用上の利点と制限があります。静的コード分析ツールを使用すると、自動化によってより多くのコードを処理できますが、あらかじめ定義された既知の種類の問題のパターンしか見つけることができません。また、結果に多数の誤検知が含まれることもあり、そうなると、限られたリソースで問題に対処するのが難しくなります。ファジー テストは、自動化も容易で、継続的に実施できる反面、少なくともある程度はランダムに行われるほか、コードの深い部分のテストが問題になることもあります。ほとんどの場合、基本的なファジー化は比較的簡単に実施できますが、重要なコード パスを完全にカバーするのははるかに困難です。

自動化された方法と比べた場合の手動のソース コード レビューのメリットとデメリットは、主に、人間のレビューアが直接関わることに関連しています。レビュー作業は、個々のテクノロジ、アーキテクチャ、およびシナリオについての参加者の経験に左右されるため、結果に大きな差が生じる可能性があります。それでも一般的には、人間のレビューアは機械のソリューションに勝ると考えるのが賢明です。レビューアは、プロセス全体を通じて学習したり、ソフトウェア コンポーネントのコンテキストを理解したり、設計者や開発者とやり取りしたりすることができます。さらに、レビューアによって提供されるフィールドバックには、問題の詳細なレポートだけでなく、高レベルのアドバイスまで含まれることもあります。

ただし、その一方で、レビューアには、人的エラー、疲労、倦怠といった問題もあります。また、レビューの範囲は通常は限定的で、品質の評価も、レビューアの主観的な信頼に基づくのが一般的であるため、難しくなります。

とはいえ、こうしたデメリットは克服可能です。たとえば、タスク固有のレビューを設計して、一度に 1 つの種類のエラーのみを対象に、コード ブロックを複数のパスで分析するようにしたり、重要なコードを複数のレビューアが個別にレビューするようにして、人的エラーが起こる可能性を抑えたりすることができます。コード レビューは、冗長性が人間の介入の限界を克服するうえで大きな潜在的価値を持つ事例の 1 つです。

手動のコード レビューは、コードの脆弱性を見つけるための究極のソリューションでも、他のアプローチに代わるソリューションでもなく、補足的なソリューションと考えるべきです。したがって、脅威のモデル化、ファジー化、コーディングのベスト プラクティスの実施に代わるものとして導入するべきではありません。コード レビューは一般に他のアプローチに比べてコストも高くなるため、主に、他のアプローチの有効性が限られている最も重要な領域や問題の範囲で使用するようにしてください。

コード レビューのプロセス

セキュリティ コード レビューは、他のセキュリティ関連の取り組み (脅威のモデル化など) のコンテキストで計画および実施された場合に最も効果的です (図 2 を参照)。コード レビューの結果から、テストや設計などの他のセキュリティ タスクを改善するなどして、さらなる価値を引き出すこともできます。

図 2 コード レビュー
図 2 コード レビュー

優れた脅威モデルは、コード レビューにとって計りしれない価値があります。コード レビューの成功のためには、レビューアが、製品の目標、設計、および実装に使用されるテクノロジをよく理解している必要があります。この最初の 2 つの領域は、脅威のモデル化でもカバーされます。そこでは高レベルの問題の発見が焦点になりますが、コード レビューに役立つ調査も含まれています。この 2 種類のセキュリティの取り組みは、互いに補い合う関係にあるのが一般的です。脅威のモデル化は、詳細なレビューの対象となるコードの重要な領域を特定するのに役立ちます。同様に、コード レビューの結果は、脅威モデルで示されたセキュリティの前提の確認 (または問題化) に役立ちます。

セキュリティ コード レビューは、品質の脅威モデルと設計仕様のレビューから始めて、その後にソース コードのレビューを行うのが理想的です。レビューを開始する前に、レビューの範囲に含まれるコードの開発作業がすべて完了している必要があります。また、比較的単純な問題を見つけておくために、利用可能なセキュリティ ツールを事前にコードに対して実行します。そのほか、既知の問題が見つからないように、既に特定されているセキュリティのバグをすべて修正しておく必要もあります。

セキュリティ チームは、最適な実施時期を特定できるように、開発ライフサイクルのなるべく早い段階でコード レビュー作業の計画を立てる必要があります。実際の計画では、全体的な投資の回収に影響を与える可能性がある組織的な意思決定についても検討する必要があります。たとえば、コード レビューの参加者を選択する際には 3 つの選択肢が考えられます。第 1 の選択肢は、コードとも製品とも無関係なレビューアです (侵入テスト チームのコード レビューの専門家など)。第 2 の選択肢は、コードとは無関係だが製品のことはよく知っているレビューアです (同じ組織の別のチームの開発者など)。第 3 の選択肢は、コードと製品の両方をよく知っているレビューアです (そのコードを開発している開発者など)。

どの選択肢にもそれぞれ利点と制限があります。製品と無関係のレビューアの場合、熟練のセキュリティ専門家が選択されるのが一般的ですが、そうしたレビューアは、その豊富な経験と広い視野により、結果に大きな違いをもたらすことができます。その一方で、製品の内部や実装の詳細に関する理解のレベルは、ほとんどの場合、製品チームのメンバより劣ります。製品の内部や実装の詳細を最もよく理解しているのは、通常、個々のコードを記述した開発者です。しかし、これらの開発者は作成者の先入観に惑わされやすく、重大なコード エラーを見落とす可能性があります。

どの選択肢も、すべてのケースに自動的に適用できる究極のソリューションではありません。コード レビューは、技術的なソリューションより人間の参加者に頼るところが大きいため、その作業に割り当てられたチームが選択を行う必要があります。グループ ミーティングで議論しながらコードを分析することによって最高の結果が得られる場合もあれば、開発者がそれぞれ自分でコードを検証することによって最高の結果が得られる場合もあります。何であれ、参加者が製品のレビューを効果的に行うことができる方法をチームで選択する必要があります。この原則は、コード レビューの計画と編成だけでなく、そうしたプロセスで直面するすべての課題に当てはまります。

レビュー作業の優先順位を決定する

コード レビューで最も大切なのは、対象になり得るコードをすべてレビューしている時間はないということを認識することです。したがって、分析の主要な (少なくとも初期の) 範囲に属する製品のコンポーネントやコードの部分を選択し、優先順位を決定することが、最大の課題の 1 つになります。この目標を達成するためには、製品の設計と、その実装に使用される個々のテクノロジの役割を理解しておく必要があります。優先順位付けは、プロジェクト全体の枠組みを定める高レベルの分析です。

優先順位を決定するには、まず最初に、脅威モデルや、製品について利用可能なその他のドキュメントを分析します。適切に作成された脅威モデルには、さまざまなコンポーネントの間の関係、エントリ ポイント、セキュリティの境界、依存関係、および設計や実装の前提に関する有用な情報が数多く含まれています。優先順位付けの目標は実装の詳細を把握することにあるため、脅威モデルから情報を集めたら、今度はその情報をコードそのものと比較して、コードの構造、品質、開発プロセスなどに関する情報を分析する必要があります。

優先順位付けは、4 つの大きなタスクに分けることができます。図 3 は、それらのタスクと、必要な情報を見つけるために役立つ問いの例です。

Figure 3 コード レビューの優先順位付けのタスクと問い (Close Figure 3)

製品の実装に使用されている環境とテクノロジを理解する
TCB (Trusted Computing Base) コンポーネント (カーネル ドライバ、サブシステム、特権サービス) ですか?
既定で開始されますか、他のプロセスと共にホストされますか、専用のプロセスとして実行されますか?
再利用可能なプラグイン、ドライバ、コーデック、プロトコル ハンドラ、またはファイル パーサーですか?
他のコード (サードパーティ製のライブラリ) を読み込んで直接呼び出しますか?
どのような基盤テクノロジが使用されていますか (RPC、DCOM、ActiveX、WMI、SQL)?
使用されている言語 (C/C++、C#、Visual Basic) およびライブラリ (STL、ATL、WPF) は何ですか?
信頼されていない入力データのソース (エントリ ポイント) をすべて列挙する
ネットワーク経由で利用できますか (TCP、UDP、SMB/NetBIOS、名前付きパイプ)?
IPC 通信を使用しますか (LPC、共有セクション、グローバル オブジェクト)?
プログラムで制御できますか (オートメーション、スクリプト)?
どのようなリソースを使用しますか (レジストリ、ファイル、データベース)?
UI がありますか、コマンド ライン引数を解析しますか、環境変数を使用しますか?
外部ハードウェアと直接通信しますか (USB ドライバ)?
誰がどのような状況でエントリ ポイントにアクセスできるのかを特定する
特権の昇格が可能なセキュリティの境界ですか?
そのエントリ ポイントは、リモート アクセス、ローカル アクセス、または物理アクセスに制限されていますか?
トランスポート レベル、インターフェイス レベル、または関数レベルのアクセス制限がありますか?
認証は要求されますか (匿名、認証ユーザー、サービス、管理者)?
認証や秘密の保護などのために使用されているメカニズムは何ですか?
その他の制約はありますか (許可/拒否リスト、証明書)?
コードの非技術的コンテキストを含める
新しいコードですか、それともレガシ コードですか?まだ開発中のコードですか、それとも保守モードのコードですか?
セキュリティ コンテキストが変わりましたか (ユーザー モードからカーネル モードに移動されたコード)?
この製品チームまたは組織のセキュリティの取り組みの履歴はどのようになっていますか?
製品のセキュリティの履歴はどのようになっていますか?
チームや組織のセキュリティの意識はどうですか?
以前のコード レビューの結果はどのようになっていますか?
自動化ツールではどこでどのような問題が検出されましたか?

最初のタスクは、コードの技術的なコンテキストの把握に関連しています。このコンテキストには、製品で使用されている個々のテクノロジだけでなく、オペレーティング システムおよびサードパーティ製品との依存関係や、開発に使用されるツールも含まれます。このタスクの目標は、製品と他のシステム、アプリケーション、サービスなどとの関係を特定することです。これらの関係に基づいて、製品が依存するコンポーネントや、製品に依存する他のソフトウェアを特定できます。セキュリティのコンテキストでは、これらの関係から、製品がシステムの他の部分に与える影響や、システムの他の部分から受ける影響を特定できます。このプロセスを通じて、危険度の高い領域が明らかになります。

危険度の高い領域は、一般に、セキュリティの境界や潜在的な攻撃ベクトルなどの概念に結び付いています。実際問題としては、それらはすべて、攻撃者が制御できるデータ (信頼できないと見なされる必要があるデータ) と、そのデータが入力されるエントリ ポイントに還元できます。チームでは、こうしたエントリ ポイントのそれぞれを、受信データに関する保証と前提との関連で分析する必要があります。そこで重要になるのが、データがいつ、どのように検証されるのかという問いです。この問いは、実際のデータの特性に関連する場合もあれば (非同期に変化するか、バラバラの順序で送信されるか)、エントリ ポイントの特性に関連する場合もあります (既定で利用可能か (ネットワーク サービスやプログラマティック インターフェイス)、ユーザーの操作 (マウス クリックやデータ ファイルのオープン) の結果として作成されるか)。

エントリ ポイントの特性は、次の大きなタスクの範囲にも含まれます。そのタスクとは、信頼関係とアクセス制御の分析です。製品にさまざまなエントリ ポイントが含まれる場合もありますが、その実際の攻撃対象領域を定義するのは、攻撃者がセキュリティの境界を越えてアクセスできるエントリ ポイントだけです。したがって、各エントリ ポイントを調査して、誰がどのような状況でアクセスできるのかを確認する必要があります。管理者特権がないとアクセスできないエントリ ポイントより、広範な機能に認証なしでアクセスできるエントリ ポイントの方が攻撃者の興味を引きやすいのは間違いありません。管理者特権が必要なエントリ ポイントのケースでは、実際の認証を行うコード (それに先行するコードを含む) をレビューするだけで済む場合もあります。

最後に忘れてはならないのが、非技術的なソースです。ここからも、コード レビューの優先順位付けに役立つ情報が得られます。コードは通常、開発者のチームによって作成され、一般にチームは、メンバの入れ替わりによって時と共に変化します。また、コードにはそれぞれ固有のセキュリティの履歴があり、それが特定の製品の範囲に収まらない場合もあります。たいていは、新しいコードの方がレガシ コードより品質が高いと想定できますが、例外もあります。開発者と話したり、以前のレビューのドキュメントを見直したり、コードのサンプルを調べたりすることによって得た情報が、大いに役立つこともあります。利用可能なリソースを最大限に活用するために、セキュリティ チームではあらゆるデータを駆使して、レビューアがプロセス全体にわたって効果的に働けるようにする必要があります。

コード レビューの戦術

ある程度の規模のアプリケーションのコード レビューでは、たいていはソフトウェアの規模と複雑さが問題になります。今日の製品は、多様なテクノロジ、プログラミング モデル、コーディング スタイル、コード管理ポリシーを使用して作成されています。実際には、完全かつ最新のドキュメントを利用できることはまずありません。レビューアは通常、許容される時間内にはとても処理できないほどの情報に直面します。これまでに、複雑な製品をレビューする際の情報過多に対処するためのプラクティス (戦術) がいくつか編み出されています。ここでは最も一般的なものを 2 つ紹介します。

コンテキスト コード分析と呼ばれる 1 つ目の戦術は、優先順位付けと高レベルの分析の作業から自然に導き出されるもので、セキュリティの境界を越えて渡された信頼されていないデータの処理に直接関連するコード領域のデータ フローの、正確でコンテキストを意識した分析に依拠しています。このように対象を絞り込んだアプローチのメリットは、攻撃を受ける可能性が高いコードに焦点を絞ることにより、最も重要なコード パスの範囲を十分にカバーできると考えられる点にあります。

コンテキスト コード分析は 2 つのパスに分割できます。最初のパスは、エントリ ポイントで、入力データに対する攻撃者の制御のレベルを把握することから始まります。そこから、プログラムの実行と共に変数やデータ構造がどのように変化するのかを追跡するためのデータ フロー分析を行います。この分析は関数単位で行われ、入力引数から始めて、すべての実行パスを辿りながら、変数と構造体の状態の変化を監視します。

プログラムの実行の過程で呼び出され、攻撃者によって制御されているデータ (エントリ ポイントから入力され、まだ検証されていないデータ) を受け取るすべての関数を、同じように分析する必要があります。同様に、データがグローバル データ構造体に反映される場所にフラグを付けて、それに対するすべての参照を後で分析できるようにしておきます。この時点では、コードについての理解を深め、その構造を知り、さらなる調査が必要な場所にフラグを付けることが、コードを辿る目的になります。

セキュリティ コード レビューでは、当然ながら、セキュリティの問題がある可能性が高いコード領域に特別な注意を払う必要があります。そのような場所の例を、図 4 に示します。

Figure 4 レビューする必要がある危険なコード パス (Close Figure 4)

ユーザーの識別、認証、およびデータの保護
承認とアクセス チェック
信頼されていないデータの前処理を行うコード (ネットワーク パケット パーサー、フォーマット リーダー)
バッファ、文字列、ポインタ、および動的メモリに対する安全でない操作
信頼されていないデータが有効な形式であることを確認する検証レイヤ
信頼されていないデータを内部データ構造に変換するコード
信頼されていないデータの解釈に関連するロジック
データそのものやそのソースの動作についての前提が含まれる場所
エラー状態の処理に関連するコード
OS リソースとネットワークの使用 (ファイル、レジストリ、グローバル オブジェクト、ソケット)
コードが実行される環境に特有の問題領域
問題のある API の使用や API コントラクトの違反

エントリ ポイントから入力される可能性があるデータを処理しないコードや、既に検証されたデータを処理するコードは、セキュリティの観点からは関連のないコードになります。したがって、そのようなコードが見つかった場合は先に進み、他の領域をレビューする必要があります。最終的には、コンポーネントのコードの大まかな理解と、要注意箇所としてフラグを付けた場所のリストが得られるはずです。そのリストを使用して、コンテキスト分析の 2 つ目のパスを開始することができます。そこでは、選択されたコード領域の詳細な調査が焦点になります。

パターンフォーカス コード分析と呼ばれる 2 つ目のコード レビュー戦術では、レビューアはコードの任意の場所から始めて、既知の種類の潜在的なセキュリティ上の脆弱性を探します。さまざまなサポート ツールを利用することができますが、ここで言う "既知" とは、必ずしも正式なパターンを指すものではなく、むしろレビューアの個人的な経験や直感を指します。脆弱性が疑われる箇所が見つかったら、それぞれについてすべてのコード パスを辿り、そのコード エラーが実際に脆弱性を表すものかどうか (攻撃者がセキュリティの境界を越えて制御できるデータを処理しているかどうか) を確認します。いずれかのレベルで適切な検証が確認された場合、そのエラーは、セキュリティ上の脆弱性とは見なされません (ただし、多層防御の問題やセキュリティ以外の問題として修正が必要とされる可能性はあります)。

パターンフォーカス コード分析は、アプリケーションについての知識が限られている場合にも使用できます。それは、選択されたコードの断片について、システム内の役割や場所ではなく、その品質や正確さに焦点を絞ってレビューすることができるからです。そのため、特定のパターン (不適切なコード コンストラクタや問題のある API 呼び出しなど) について大量のコードをカバーすることができます。しかし、潜在的なコード エラーをアプリケーション全体のコンテキストから離れて分析する場合は、一般に、それが本当の問題かどうかを判断できるだけの情報は得られません。そのため、重大なバグを見逃したり、適切でないバグを修正したりして、アプリケーションに長期的な影響を与えてしまう可能性もあります。バグがローカルのコンテキストでしか説明されていないと、より高いレベルでより完全な検証を行わずにローカル レベルで修正しようとしてしまいがちですが、そのような方法でコード エラーを修正すると、冗長な検証、混乱の増大、パフォーマンスの低下、コード管理の一般的な問題などの原因になる可能性があります。

脆弱性の種類
セキュリティ リソース
  • 信頼できるコンピューティングと SDL
  • MSDN Security Developer Center
  • 『The Security Development Lifecycle』 (Michael Howard および Steve Lipner、Microsoft Press、2006 年)
  • 脅威のモデル化 : セキュリティ設計の欠陥を STRIDE 手法で明らかにする (S. Hernan、S. Lambert、T. Ostwald、および A. Shostack、MSDN Magazine 2006 年 11 月号)
  • 『19 Deadly Sins of Software Security』 (M. Howard、D. LeBlanc、および J. Viega、McGraw-Hill Osborne Media、2005 年)

セキュリティ コード レビューの間は、常に攻撃者の視点に立ち、攻撃者が制御できるものすべてに目を向けるように心がけてください。コード レビューは、危険な API やデータ コピー操作を探すためだけに行うものではありません。セキュリティに関しては細部が重要です。些細な欠陥が悪用され得ることは既によく知られています。このセクションでは、コード エラーの種類の例をいくつか紹介しますが、ここですべてを取り上げることはできません。潜在的な脆弱性についてのより包括的なソースについては、「セキュリティ リソース」を参照してください。

コードのセキュリティ上の脆弱性の一般的な概念は、多くの場合、バッファ オーバーフローに結び付いています。信頼されていないデータの処理のコンテキストでも、範囲やデータ型に関連するコード エラーがセキュリティの問題の原因になることがよくあります。ただし、このグループの脆弱性は、バッファ オーバーフローのみに限定されません。バッファ オーバーフロー状態は、データをそのサイズに配慮せずにスタック、データ セグメント、またはヒープにコピーすることに関係しています。その結果、定義された範囲を超えた書き込みが行われて、プログラム フローを制御する構造体やその他の機微なデータが上書きされる可能性がある危険な状況を引き起こすことがあります。

範囲の検証に関連する、同じくらい一般的で重大なコード エラーの例となるのが、バッファ外の読み取りと書き込みです。これらの問題は、信頼されていないソースから受け取ったポインタを使用する場合 (ポインタを Cookie やハンドル値として使用する場合) や、誤って計算されたインデックスやオフセットを使用する場合によく発生します。これらの問題が発生すると、攻撃者がプロセス メモリの任意の場所 (データ変数や関数ポインタなど) にアクセスしてプログラムの動作を変更できるようになります (任意のコードを実行するなど)。

計算の領域では、数値型の変数のさまざまな変換 (加算、減算、乗算など) により、計算がその定義された範囲を出て自動的にラップして、整数のオーバーフローやアンダーフローを引き起こす可能性があります。これが問題になるのは、変換後の変数が使用されるのと同時に、その変数が別の場所で、変換されていない状態や別の形で変換された状態で使用された場合です。結果として、既存のチェックでは対応しきれずに、バッファ オーバーフローや、バッファ外の読み取りと書き込みが発生する可能性があります。

そのほかに、数値型変数の符号に関連する問題もあります。プログラムが扱う抽象エンティティには、負の値の意味が定義されていないものも数多くあります。たとえば、文字列のバイト長や配列の要素数では、負の値は実際には意味をなしません。これらの数量を操作するために符号付きの型を使用すると、さまざまな問題の原因になります。

型キャスト (明示的なものも暗黙的なものも) もまた、安全でない可能性があるデータ要素の変換のひとつです。キャストでは、アイテムが (ビット数の観点から) 大きくなることがあるため、符号付きビットが新しい値に反映される可能性があります。同様に、値の切り捨てが発生する可能性もあります。この場合も、問題が表面化するのは通常、データ アイテムがプログラムのある場所では変換された状態で使用され、別の場所では変換されていない状態や別の形で変換された状態で使用された場合です。

動的メモリの使用に関連するコードのセキュリティ上の脆弱性もあります。初期化されていないデータ要素を含むプログラムの動作が定義されていない場合がありますが、攻撃者は、変数を初期化する API が呼び出される前に、初期化されていない変数を取得または設定する API を呼び出すことによって、それらの変数を制御することができます。初期化されていないメモリが攻撃者に返されると、さまざまな問題の原因になります。たとえば、ヒープに割り当てられるリソースは、さまざまなセキュリティ コンテキスト (クライアント) によって使用されているさまざまなスレッドによって共有されている可能性があるため、あるクライアントから別のクライアントに機微な情報が漏洩する可能性があります。また、他のセキュリティの欠陥をより広範に悪用するのに役立つ情報を攻撃者に与えてしまう可能性もあります (アドレスの予測に役立つ情報など)。そのほか、メモリの二重解放、既に解放されたメモリの使用、メモリ リーク、メモリの枯渇に関連する問題もあります。そこから派生する問題は、サービス拒否や情報漏洩から悪意のあるコードの実行まで、多岐にわたります。

まったく異なるグループの問題として、同期とタイミングのエラーに関連するコードのセキュリティ上の脆弱性もあります。その良い例となるのが、TOCTOU (time-of-check, time-of-use) 競合状態です。セキュリティ チェックや浄化チェックは、チェックが行われてから実際に使用されるまでの間に攻撃者によって変更される可能性があるデータやリソースに対して行われても、何の役にも立ちません。検証は、非同期に変更される可能性がないデータのプライベート コピーに対して行う必要があります。リソースは、削除されたり置き換えられたりしないように適切に参照する必要があります。

マルチスレッド環境では、共有されているオブジェクトやリソースへのアクセスの同期についてのコード (サービス、カーネル コンポーネント) の要件が特に厳しくなります。攻撃者が関数を同時に呼び出したりそれらを非同期にキャンセルしたりするために使用できるプログラマティック インターフェイスは、競合状態を引き起こす原因になります。ロックが失われたり、ロックが削除されたのに保持されていると仮定したり、インターロック操作を誤って使用したり、一連の不整合なロックを使用したりすることによって引き起こされる問題は、安全でないメモリ操作やデッドロックの原因になります。

オブジェクトの有効期間の管理の問題について言えば、コードでグローバル データを操作 (公開) して他のスレッドが新しいオブジェクトを使用できるようにする場合、そのオブジェクトの状態が一貫している (通常は完全に初期化されている) 必要があります。オブジェクトの有効期間の適切な管理は、適切な参照カウントのメカニズムによって決まります。オブジェクトの破棄とクリーンアップのロジックが壊れていると、コード (プラグインなど) を早い段階でアンロードして、まだ使用中のメモリを解放するための手段を攻撃者に与えてしまう可能性があります。

信頼されていないデータの操作に直接関連してはいないが、その実際の解釈や、プログラムの動作や結果に与える影響に関連するセキュリティ コード エラーもたくさんあります。その古典的な例となるのが、攻撃者によって送信されたデータが他のコンテンツ (スクリプト、Web ページ、コマンド ライン、書式文字列など) をパラメータ化するために使用された場合に発生するインジェクション攻撃です。攻撃者は、特殊なエスケープ文字や制御シーケンスを使用することにより、信頼できないデータを、実行されるスクリプトやコマンドの一部として解釈されるようにすることができます。また、作成または使用されようとしているリソース (ファイル、ソケット、レジストリ、共有セクション、グローバル オブジェクトなど) の名前とパスに、信頼できないデータが使用された場合にも問題が発生します。ディレクトリ トラバーサルや正規化の問題は、あらゆる種類のリダイレクトに対するコードの脆弱性を引き起こします。その結果、攻撃者に制御されているリソースが使用されたり、機微な情報 (ユーザーの秘密情報やネットワーク資格情報など) が漏洩したりする可能性があります。

セキュリティ上の脆弱性は、データの送信元と送信先 (クライアント ID、ネットワーク アドレス、セッション ID、コンテキスト ハンドル)、受信される順番 (ネットワーク メッセージ、API 呼び出し)、および量 (データが多すぎる、または少なすぎる) に関する前提の誤りによって引き起こされることもあります。攻撃者は、名前を持つオブジェクトを本物の製品によって作成されたり使用されたりする前に作成したり (ネーム スクワッティング攻撃)、ディスク領域を占有したり、ネットワーク通信をブロックまたはリダイレクトしたりすることによって、多くの場合、コードが実行される環境をある程度制御することができます。これらの状況が適切に処理されていないと、特権の昇格やサービス拒否などの攻撃に対するコードの脆弱性の原因になります。そのほか、コードで使用されているテクノロジに関する前提 (セキュリティ保証に関する前提 (通信チャネルの整合性) や、API の内部構造に関する前提 (API のコントラクトに従わずに、間違った組み合わせのパラメータを渡したり、戻り値をチェックしなかったりするなど)) に起因する一般的な問題もあります。

コード レビューの成果は、多くの場合、コード レベルの問題として分類される問題の発見だけにとどまりません。レビューアは実際の実装のレベルで作業するため、設計レベルのドキュメント (仕様書や脅威モデルなど) を有効に利用することはあっても、そのデータを当てにしてはいけないことになっています。したがって、コード レビューの段階で、設計レベルの問題や、仕様書と実装の不整合が見つかることもあります。よく見つかる種類の問題には、危険な機能の公開、プロトコルの実装の間違い、カスタムの擬似セキュリティ メカニズムの使用 (認証スキームやアクセス チェックなど)、セキュリティ バリアを迂回する方法の発見などがあります。

結果を処理する

セキュリティ コード レビューは、製品のセキュリティが向上しなければ成功とは言えません。コード レビューの出力は、開発チームが製品に有意義な変更を加えるのに役立つものである必要があります。このレビューに基づく改良の実現は、完全なドキュメントと正確なトリアージという 2 つの実際的な要件にかかっています。

特定された各コードのセキュリティ上の脆弱性のドキュメントには、その問題を見つけて把握するために必要な詳細情報がすべて含まれている必要があります。これには、欠陥のあるコードへのポインタ、問題の説明、それが脆弱性である根拠などが含まれます。修正のためのアドバイスを追加するのも有効ですが、実際の解決策を選択したり用意したりするのはコードの所有者の責務です。データが欠けていたり、コード エラーがセキュリティ上の脆弱性である理由が明確でなかったりすると、製品チームからさらなる調査を要求されることになりますが、それは、リソースや時間の制約から常に可能とは限りません。

もう 1 つの重要な要件は、コードの脆弱性の正確なトリアージに関連することです。問題の重大度の設定が低すぎると、製品チームによって修正されない可能性があります。一方、重大度の設定が高すぎると、実際の影響がより大きい問題が後回しにされる可能性があります。トリアージのプロセスは、セキュリティ バグのしきい値の品質に大きく依存しますが、コード レビューア (トリアージを行う人) や開発者 (トリアージの結果に基づいて作業する人) の優先事項の理解度にも左右されます。前にも述べたように、セキュリティ コード エラーのトリアージは、利用可能な悪用防止メカニズムの有無に影響されるべきではありません。

まとめ

コード レビューの作業からは、単なるセキュリティの問題のリストにとどまらない有用な情報が得られます。レビューアは、可能であれば、コード カバレッジ、特定のコード領域の信頼度、コードの再設計やクリーンアップの一般的なアドバイスなどもドキュメントに含める必要があります。コード レビューは、組織の知識を深め、セキュリティの意識を高め、セキュリティ ツールの効果を向上させるための貴重な機会にもなります。さらに、開発チームがコード レビューの結果を、将来の製品のセキュリティの取り組みの優先順位付けに役立てることもできます。

ソフトウェアのセキュリティ上の脆弱性は、その性質や実際の影響は時と共に変化しますが、なくなることはありません。自動化されたセキュリティ ツールでは、ますます多くのコード エラーを特定できるようになっていますが、それでもすべての脆弱性が検出されるわけではありません (検出されない場合もあれば、多数の誤検知に紛れてしまう場合もあります)。手動のソース コードの分析は、こうした実証済みのツールに代わるものではありませんが、多くの場合、それらと有効に組み合わせることができます。

手動のコード レビューは、コストが高く、困難で、参加者の経験や関与に大きく依存する方法です。それでも、プロジェクトで製品やその重要なコンポーネントのセキュリティについて許容されるレベルの信頼を得るためには、さまざまな状況でこの投資が必要になります。ツールによって見逃される問題も、経験を積んだレビューアなら発見できます。セキュリティの問題の原因が人間にある以上、その解決にも人間が関わるべきです。

Michal Chmielewski は、マイクロソフトの SWI Attack チームでセキュリティ ソフトウェア エンジニアを担当しています。製品のセキュリティ上の脆弱性を分析するスペシャリストです。

Neill Clift は、マイクロソフトの SWI チームでセキュリティ ソフトウェア エンジニアを担当しています。カーネルやドライバの問題の専門家です。

Sergiusz Fonrobert は、現在 SWI Attack チームのセキュリティ ソフトウェア エンジニアです。さまざまなマイクロソフト製品の侵入テストを実施しています。

Tomasz Ostwald は、現在 SWI チームのセキュリティ プログラム マネージャを担当しています。マイクロソフトからリリースされる製品の最終的なセキュリティ レビュー (Final Security Reviews) を実施しています。


ファジー テスト: チーム システム向けにカスタム テスト インターフェイス プロバイダを作成する

ファジー テスト

チーム システム向けにカスタム テスト インターフェイス プロバイダを作成する

Dan Griffin
コードのダウンロード : : Fuzzing2007_11.exe (301KB)
この記事の内容 : :
  • Visual Studio Team Edition for Testers
  • ファジー テスト
  • テスト インターフェイス プロバイダを作成する
  • Fuzzer をビルド、テスト、および展開する
この記事は次のテクノロジを使用しています:
Visual Studio 2005 SDK、VSTS
コンテンツ
Visual Studio Team System とは
ファジー テストとは
テスト インターフェイス プロバイダを作成する
ビルドする
展開する
デバッグする
Fuzzer がもたらすもの

オーランドで開催された Microsoft® Tech•Ed 2007 カンファレンスに出席した私は、The Learning Center のある開発者ブースに参加する機会に恵まれました。そこで最も驚いたのは、最新のアプリケーション ライフサイクル管理 (ALM) ツールを取り巻く熱狂的な雰囲気でした。また、アジャイル プログラミングやテスト駆動開発 (TDD) といった今話題の方法論に関する話を数多く聞きました。その結果、マイクロソフトの最新の ALM スイートである Visual Studio® Team System (VSTS) 製品に大きな関心が寄せられました。

この記事では、VSTS が提供するテスト担当者向けの優れた機能と拡張性に焦点を当てます。ただし、TDD についてはこの記事で触れません。代わりに、VSTS のソフトウェア テスト機能を拡張する方法を示します。例として、"ファジー化" (後ほど詳しく説明します) と呼ばれるセキュリティ テストのサブセットを使用します。

Visual Studio Team System とは

VSTS のサーバー側コンポーネントは、Team Foundation Server (TFS) と呼ばれます。クライアント側コンポーネントは、Visual Studio のいずれかのチーム エディションです。VSTS と TFS の詳細については、msdn2.microsoft.com/teamsystem を参照してください。

Visual Studio Team Edition (VSTE) 製品は、技術的には Visual Studio 2005 IDE に対する一連の拡張機能で構成されています。それぞれのエディションが、特定の専門分野 (アーキテクト、開発者、テスト担当者、データベース プロフェッショナル) に関連する拡張機能を提供します。その目的は、職務固有の機能を一貫したユーザー インターフェイスにまとめるためです。

Visual Studio の優れた点の 1 つは、新たなリリースのたびに拡張性が向上していることです。VSTE はその一例ですが、製品の拡張性をより明確に測る基準は、製品の製造元に属していない第三者による拡張がどの程度容易かということです。

そこでこの記事では、Visual Studio 2005 Team Edition for Software Testers の拡張性を説明するために、最新の Visual Studio SDK に収録されているテスト インターフェイス プロバイダ (TIP) の変更について考察します。また、プラグインのテスト、展開、およびデバッグについても詳しく説明します。最終的な目標は、特にセキュリティ テスト担当者の間で最近非常に一般的になっているテスト方法であるファジー テストを実装する TIP です。

ファジー テストとは

ファジー テスト (ファジー化) は、例を使って説明するとよくわかります。最近は、ファイルとネットワークの 2 種類のデータ パーサーを検証する際にファジー化が最もよく使用されます。

ファイル パーサーの一例が Microsoft Word です。Word ファイル パーサーではどのようなファジー テストが必要になるでしょうか。ランダム データを含んだ任意の長さの Word ドキュメントを大量に作成するとします。これらのドキュメントは、Word 自体で作成されるのではなく、任意のソースのバイナリ データをディスク上のテスト ファイルに渡す方法で作成されます。このランダムなドキュメントをそれぞれ Word で開き、その結果を確認します。

この実験では、3 とおりの結果が予想されます。1 つ目は、大多数のテスト ファイルで、ファイル形式が認識されない、またはファイルが破損して開くことができないというエラーが Word で発生するというものです。2 つ目は、大量のファイルのうち数ファイルが、認識可能なコントロールと出力可能な文字の組み合わせを実際に含んでいるというものです。このファイルは何の問題もなく開きます。3 つ目の最も興味深い結果は、ドキュメント パーサーが予期しないデータを数ファイルが含んでいるというものです。この場合はプログラムがクラッシュする可能性があります。

セキュリティ テスト担当者にとって、この 3 番目のテスト ドキュメントは実に興味深い問題です。ファイル パーサーがクラッシュした場合は、そこにバグがあることを意味します。バグがあるということは、悪用が可能ということです。たとえば、バグによってスタックまたはヒープが破損し、クラッシュにつながる場合があります。一般に、この種のバグはネットワーク環境に大きな懸念をもたらします。それは、さまざまな種類のファイルが電子メールの添付ファイルとして配信される可能性があるからです。ほとんどのユーザーは添付ファイルを種類に関係なく開くことが、経験からわかっています。

同様の問題がネットワーク パーサーの領域にも存在します。一般的な PC、サーバー、または組み込みネットワーク デバイスで認識される多数のプロトコル (DHCP (動的ホスト構成プロトコル)、DNS、HTTP、SMB (サーバー メッセージ ブロック)、SMTP など) を考えてみてください。各プロトコルの実装にはネットワークで発生したパケット データのパーサーが含まれており、それぞれのパーサーはおそらく何らかの複雑なロジックを伴っています。この種のコードには、多数のエッジ ケース (頻繁には発生しないケース) が発生する傾向もあり、コードの検証を難しくしています。

ネットワーク プロトコル コードをテストするアプローチの 1 つがファジー化です。ただしこの場合は、前述の Word パーサーのところで説明した、ランダム データを渡すだけのアプローチの変化形を提案します。

たとえば、DHCP クライアントをテストするとします。この DHCP クライアント コードには特に問題となる部分がありますが、その部分のコードがパーサーのより堅牢と見られる別の部分によってネットワークから保護されているとします。この場合、単にランダムなデータで DHCP クライアントをファジー化するのは時間の無駄であると思われます。それは、コードの堅牢な部分で正しくないデータの大部分が除去されると予想されるからです。私なら、より対象を絞ったアプローチを使用します。

事実、私の経験では、このようにファジー化を実施するのが最も効果的です。DHCP の例に戻ると、単にランダムなデータをクライアントにいっせいに送るのではなく、既知の有効データ、意図的な不良データ、およびランダム データの組み合わせを使用するようにファジー テスト (Fuzzer) を構成します。既知の有効データを使用するのは、ここでは関係のないパーサーのコンポーネントをスキップするためです。意図的な不良データを使用するのは、コード内で欠陥として認識している部分または疑われる部分を利用するためです。最後に、ランダム データを使用するのは、単に何が起きるかを確認するためです。

ファジー化の過程で、疑わしい動作に焦点を絞ると、それに伴って私の方法は進化します。たとえば、テスト用の DHCP パケットの一部に最初からランダム データが含まれていた場合は、その後に既知の有効データを使用してパーサーを深く調べることができます。エラーを発見した場合は、意図的な不良データを送信してパーサーの悪用を試みることができます。このヒューリスティックなアプローチにより、人間の直感とコンピュータによる自動化という双方の長所を活かすことができます。

ファジー化は実に強力なツールですが、テスト カバレッジに関してはまだ比較的未知の領域にあります。そこで私が呼びかけたいのは、自分がよく知っているテクノロジ、あるいは少なくとも関心を持っているテクノロジを選び、そのテクノロジ向けの Fuzzer を作成しようということです (もちろん実行に移すのはこの記事を読み終わった後です)。これできっとバグが見つかります。

一例を挙げましょう。今年サンフランシスコで開催された RSA カンファレンスで、見事なファジー化のデモンストレーションを見ました。これに触発された私は急いでホテルの部屋に戻り、業務で深くかかわっていたテクノロジ向けの Fuzzer を作成しました。カスタムの Fuzzer を使用して、私は上記の反復型アプローチ、つまりランダム データから始めてその後に微調整するアプローチを適用し、数時間の間に貴重な発見をしました。ユーザーとして、LocalSystem として実行されているコード内でアクセス違反を起こすことができたのです。

テスト インターフェイス プロバイダを作成する

この記事に付属のサンプル コードは、単純な Fuzzer の実装です。これには 2 つの目的があります。作動中のファジー テストを見せること、および VSTE の自動化フレームワークに統合されたファジー テストの例を示すことです。

サンプル コードには、次の 4 つのプロジェクトが含まれています。

  1. ConsoleApp はテスト対象のアプリケーションです。このコードは簡潔です。このアプリケーションは、1 行のテキストを出力するか、ハンドルされない例外をスローすることで応答します。
  2. MyTest は、カスタムのテスト タイプ プロバイダです。このコードはサンプルからほとんど変更していませんが、MyTestAdapter の名前を FuzzTestAdapter に変更し、実際のテストを実施するコードを追加しました。
  3. MyTestUI は、カスタムのテスト ユーザー インターフェイスの C++ コードです。このコードは元のサンプルから変更していません。
  4. TestProject は、デバッグ用のスタートアップ プロジェクトです。再作成は簡単です。新しい Test Project を作成し、カスタムのテスト タイプを追加するだけです。

これらのプロジェクトについては、この記事の後半で詳しく説明します。

サンプルをビルドするには、Visual Studio 2005 SDK が必要です。この SDK は、microsoft.com/downloads/details.aspx?familyid=51A5C65B-C020-4E08-8AC0-3EB9C06996F4 からダウンロードできます。SDK の最新バージョンは 4.0 です (発行日は 2007 年 2 月 28 日)。このバージョンには、私がベースとして使用した元のサンプル VSTE TIP が収録されています。ただし、元のサンプルのビルド、インストール、実行ではいくつかの問題が発生しました。既存の "MyTest" サンプルを、この記事で説明する "FuzzTest" サンプルに変えるために必要な変更はわずかでしたが、実行中のプラグインをデバッグするまでにはかなりの労力を要しました。製品チームの支援を受け、何度も試行錯誤を繰り返して、これらの問題を解決しました。その過程についてはこの後詳しく説明します。

繰り返しますが、元の MyTest サンプルをこの記事に付属の FuzzTest サンプルに変えるために行った変更は簡単に理解できます。実際に、元のサンプルと変更後のサンプルのディレクトリ ツリーを比較するために –T スイッチを指定して windiff.exe を実行すると、興味深く有益な短い変更リストが表示されます。

最初に必要な変更は、ベースの MyTest サンプルにない 2 つのファイル (少なくとも前述の Visual Studio SDK バージョンには含まれていません) を取得することでした。2 つのファイルとは、mytest.ico と mytestui.rc です。どちらのファイルも VSTS 製品チームから提供されたもので、私の FuzzTest サンプル コードに組み込まれています。この 2 つのファイルはソリューションの mytestui サブディレクトリに配置されます。

次にコードを変更したのは、mytestui プロジェクト ファイルです。具体的には、ビルド後のコマンドが相対パスを使用しており、サンプル プロジェクトをその既定の場所 (つまり SDK インストール ツリー) 以外のディレクトリでビルドしようとするとエラーが発生していました。問題のコマンドは、Visual Studio SDK 付属のツールであるコマンド テーブル構成コンパイラ (ctc.exe) を起動します。CTC ファイルの詳細については、msdn2.microsoft.com/bb165048 を参照してください。

この問題に対処するために、私は ctc.exe がその既定の絶対パスから起動されるようにプロジェクトを変更しました。このアプローチ自体にも欠点はありますが (たとえば SDK が C: ドライブにインストールされていない場合)、少なくとも探す対象はわかります。

余談ですが、上記の変更について説明したのは、これらの変更が FuzzTest サンプルだけでなく、追加の実験が必要になったときに元のサンプルをビルドする場合にも関係するからです。展開とデバッグについてもいくつかの重要な問題を認識しておく必要がありますが、これについてはこの記事で後ほど説明します。

ベースの MyTest サンプルを新しいテスト プラグインに変えるための次のステップは、元の MyTestAdapter クラス (私が削除した mytest\myhostadapter.cs 内) を mytest\fuzztestadapter.cs 内の新しい FuzzTestAdapter 実装で置き換えることでした。この 2 つのファイルを一時ディレクトリにコピーし、mytestadapter.cs および fuzztestadapter.cs に対して windiff.exe を実行すると、新しい TIP の作成に必要なコード変更が基本的にすべて表示されます。実際には、興味深い変更の大部分が Run 関数内に存在します (図 1 を参照)。

Figure 1 IBaseAdapter::Run メソッドの実装 (Close Figure 1)
[SuppressMessage("Microsoft.Design", "CA1031")]
public void Run(ITestElement testElement, ITestContext testContext)
{
MyTestAssertHelper.ParameterNotNull(testElement, "testElement");
MyTest test = testElement as MyTest;
MyTestAssertHelper.ParameterNotNull(test, "testElement");
MyTestResult result = new MyTestResult(
       new ComputerInfo(Environment.MachineName), m_runId, test);
Stopwatch timer = null;

try
{
// Start the timer for this test.
timer = new Stopwatch();
timer.Start();

// This is a FUZZ Test: Loop until the test breaks, then report back
// loop through unicode chars and pass into method
bool ErrorFound = false;
char ch;
for (int i = 0; i < char.MaxValue; i++)
{
       ch = Convert.ToChar(i);
       string testParam = ch.ToString();
       string CommandLine = test.CommandLine.Replace(
              "<FuzzTestString>", "\"" + testParam + "\"");
       int spaceIndex = CommandLine.ToUpper().IndexOf(".EXE ") + 4;

       Process p = new Process();
       p.StartInfo.FileName = CommandLine.Substring(0,spaceIndex);
       p.StartInfo.Arguments = CommandLine.Substring(spaceIndex+1);
       p.StartInfo.UseShellExecute = false;
       p.StartInfo.CreateNoWindow = true;
       p.StartInfo.RedirectStandardInput = true;
       p.StartInfo.RedirectStandardOutput = true;
       p.StartInfo.RedirectStandardError = true;
       p.Start();

       StreamWriter sIn = p.StandardInput;
       StreamReader sOut = p.StandardOutput;
       StreamReader sErr = p.StandardError;
       sIn.AutoFlush = true;
       sIn.Write(Environment.NewLine);
       string OutputString = sOut.ReadToEnd();
       string ErrorString = sErr.ReadToEnd();
       p.WaitForExit();

       if (ErrorString != "")
       {
       // Record the process exit code.
       ErrorFound = true;
       result.ProcessExitCode2 = -1;//failed
       result.Outcome = TestOutcome.Failed;
       result.ErrorMessage =
              "Test failed with input: " +
              CommandLine +
              ", Error Message: " +
              ErrorString;
       timer.Stop();
       break;
       }
}

if (!ErrorFound)
{
       // all tests succeeded:
       result.ProcessExitCode2 = 0; // success
       result.Outcome = TestOutcome.Passed;
       timer.Stop();
}
}
catch (Exception ex)
{
       result.Outcome = TestOutcome.Failed;
       result.ErrorMessage = ex.ToString();
}
finally
{
       if (timer != null)
       {
       timer.Stop();
       result.Duration = timer.Elapsed;
       }
}
testContext.ResultSink.AddResult(result);
}

図 1 の Run 関数のコードを分析すると、新しい FuzzTestAdapter のしくみが明らかになります。つまり、FuzzTestAdapter タイプのテストを VSTE で実行する場合は、最初のステップ (冒頭のオブジェクトの初期化が終わった後) としてタイマが開始されます。パフォーマンス テストなどとは異なり、ファジー テストではテストの実行時間は必ずしも興味深い統計ではありませんが、TestResultMessage クラスの組み込み機能としてサポートされているので、そのまま残しました。

Run での次のステップでは、対象のテスト インスタンスの実行方法を示すコマンド ライン構成オプションが抽出されます。対象のテスト バイナリの完全パスの後に、プラグインがプレースホルダ文字列 "<FuzzTestString>" を予期している点に注意してください。この文字列全体は、MyTest クラスの CommandLine メンバを通じて取得されます。この文字列の目的は、別のテストで TIP を再利用するための便利な方法をユーザーに公開することです。私の Fuzzer TIP では、テストがループするたびにコマンド ラインが変更されるので、この文字列がさらに役に立ちます。

Run メソッドの説明に戻りましょう。次のステップでは、System.Diagnostics.Process オブジェクトがインスタンス化されて、テスト プログラムがローカル コンピュータ上で起動されます。テスト入力 (またはその他の理由) が原因で、テスト プロセスから例外がスローされるか、またはエラーが返された場合は、テスト エラーのフラグが付けられます。私の Run メソッドの実装には [SuppressMessage] 属性を付加していることに注意してください。この状況ではこの属性が必要です。これは、テスト プロセスの実行がすべての例外をキャッチするハンドラでラップされているからです。これは一般には不適切な手法と見なされ、VSTS の静的コード アナライザでも既定でそのように警告されますが、この場合は、テストが正常に終了しなかったすべてのシナリオをエラーとしてキャッチし、追跡するために必要となります。

Run の説明の締めくくりとして、また私の TIP によって実装される特定タイプのファジー テストを説明するために、前の 2 つの段落で説明したロジックの大半がループにラップされ、そのループが char マネージ型によって受け取られる値ごとに反復処理されることを図 1 で確認してください。最終的には、テスト バイナリ (このサンプルの ConsoleApp.exe) が char 値ごとに呼び出され、ループが完了するか、またはテスト プログラムがその範囲のいずれかの値の処理に失敗するまで継続されます。当然ながら、デモのためにテストは失敗するようになっています。そうでないと、興味深い例にはなりません。

ConsoleApp テストは、ファジー化を使用することで、単純なプログラマティック インターフェイスのエラーでも明らかにできることを示します。この場合、インターフェイスは、処理される文字値の範囲に関するリスクを伴う暗黙の前提に基づいて作成されています。ConsoleApp コードから抜粋した次のスニペットは、この前提を示しています。

Int32 arraySize = Math.Max(Math.Max('Z', 'A'), Math.Max('z', 'a')) + 1;
charCountArray = new Int32[arraySize];
char c = inputString[0];
charCountArray[c] += 1;

このコードの inputString 変数は、FuzzTestAdapter から受け取る 2 番目のコマンド ライン引数です。文字値が ASCII の大/小文字の英字の範囲外にあると、"array index out of bound" (範囲外の配列インデックス) エラーが発生します。Fuzzer がその文字値をループ処理すると、コードのエラーを引き起こす文字値が即座に見つかります。これが発生すると、ハンドルされない例外で ConsoleApp process が終了します。この結果は、Fuzzer がその Process p オブジェクトを通じて取得し、VSTE に返されます。

元のサンプル コード内の MyTestAdapter クラスを FuzzTestAdapter クラスで置き換えた後は、置換前のクラスへの参照の名前を置換後のクラスへの参照に変更することで、私の実装での作業は完了しました。

ここまでは、元の TIP サンプルを私の FuzzTestAdapter に変えるために必要な変更について説明しました。次にビルド、展開、およびデバッグについて見ていきましょう。

ビルドする

サンプルの FuzzTestSample.sln ソリューションをビルドするには、VSTE でこのソリューションを開き、最上位メニューで [ビルド]、[ソリューションのビルド] の順に選択します。SDK 付属の基本的なサンプル ソリューションから始める場合は、最初に 2 つの不足ファイルをこの記事に付属のコードから取得する必要があります。

展開する

元の TIP サンプルと更新された Fuzzer バージョンの両方に、展開バッチ スクリプトが含まれています。テスト用の TIP バイナリをインストールする場合は、このスクリプトを使用することをお勧めします。このスクリプトは、バイナリがビルドされたコンピュータで、Visual Studio コマンド ウィンドウから実行されることを前提とします。

スクリプトを実行する前 (ただし当然ながらソリューションをビルドした後) に、展開フェーズに向けて Visual Studio をシャットダウンしておいた方が安全であることがわかりました。これは確かに不便ですが、この手順を実行しておかないと、展開中に一部のファイル コピー操作でエラーが発生します。

Visual Studio コマンド ウィンドウを実行するには、[スタート]、[すべてのプログラム]、[Microsoft Visual Studio 2005]、[Visual Studio ツール]、[Visual Studio 2005 コマンド プロンプト] の順に選択します。その後に、deploy.bat セットアップ スクリプトを実行します。私はこのスクリプトに少し変更を加えました。読者のみなさんも、スクリプトの実行場所によっては元のスクリプトを若干調整する必要があるかもしれません。バイナリを展開するために copy コマンドと xcopy コマンドで使用される相対パスを確認してください。また、スクリプトで最後に実行されるコマンドが regpkg.exe の呼び出しであり、その呼び出しが SDK の Visual Studio Integration Tools ディレクトリへの相対パスに基づいていることに注意が必要です。私は、SDK の既定のインストール ディレクトリが使用されるという前提を基に、このパスを %ProgramFiles% Windows 環境変数を使用する絶対パスに変更しました。

最後に、次の最終ステップを忘れないでください。バッチ ファイルにはこのステップが組み込まれていません。Visual Studio コマンド ウィンドウから devenv /setup コマンドを実行します。このコマンドによって、プラグインのリソース メタデータが他のインストール済み Visual Studio パッケージにマージされ、新しいプラグインのインストールが完了します。

この記事を執筆するにあたって、TIP が実際にどのように使用されるかを Visual Studio 製品グループにたずねてみました。TIP はテスト タイプごとに定義する必要があり、それが TMI (テスト管理インターフェイス) によってインスタンス化されます。TMI は、この TIP を使用して記憶域からテストを読み込み、新しいテストを作成し、テストを保存し、結果を解釈します。

デバッグする

展開が完了したら、次に新しいタイプのテストを作成して実行します。これを行うために私が使用した手順は次のとおりです。

  1. VSTE で FuzzTestSample ソリューションを再び開きます。
  2. TestProject を既定のスタートアップ プロジェクトとして設定します。そのためには、ソリューション エクスプローラ ウィンドウで [TestProject] を右クリックし、[スタートアップ プロジェクトに設定] をクリックします。これを行うと、図 2 のスクリーンショットの右側に示されているように、ソリューション エクスプローラで TestProject の名前が太字で表示されます。
  3. ソリューション エクスプローラで [TestProject] をもう一度右クリックし、[追加]、[新しいテスト] の順にクリックします。
  4. [新しいテスト] ウィンドウが開き、"My Test" というタイトルのテスト タイプが表示されます。これはサンプルの TIP から取得した新しいテスト タイプです。これが表示されれば、登録ステップは正常に終了しています。[My Test] アイコンを選択し、[OK] をクリックします。
  5. 新しいテストの "コマンド ライン" フィールドで、既定のエントリを ConsoleApp.exe テスト プログラムの完全パスで置き換え、予期されるコマンド ラインの値を付加します (前のセクションの Run ルーチンに関する説明を参照してください)。たとえば、「c:\projects\FuzzTestSample\ConsoleApp\bin\Debug\ConsoleApp.exe ProcessString <FuzzTestString>」と入力します。図 2 の左側中央のペインを参照してください。
  6. F5 キーを押してテストを実行します。

図 2 VSTE に示されたサンプル テストの結果
図 2 VSTE に示されたサンプル テストの結果(画像を縮小するには、ここをクリックします)
図 2 VSTE に示されたサンプル テストの結果
図 2 VSTE に示されたサンプル テストの結果 (画像を拡大するには、ここをクリックします)

このアプローチには、TIP をデバッガ内で実行できるという利点があります。たとえば、F5 キーを押す前に、MyTest\MyTest\FuzzTestAdapter.cs 内で Run 関数にブレークポイントを設定し、この関数がさまざまな文字値で ConsoleApp.exe を起動するのを監視できます。もちろん実稼働環境においては、テストの実行が著しく遅くなることから、デバッガでプラグインが実行されることはないでしょう。

この特別なテストは、わずかな時間で完了します。完了すると、図 2 のスクリーンショットの一番下に示されているテスト エラー ペインが表示されます。このペインに表示されるエラー テキストから、予想されたとおりに、英字以外の文字によって IndexOutOfRange 例外が発生したことがわかります。

最初の展開が成功した後にカスタム テスト タイプにコード変更を加える場合は、私が修得した次の方法が役に立ちます。実験に基づいたこの一連のステップは、プラグインを再度デバッグできるように再展開するために最低限必要になると思われます。

  1. 必要なコード変更を行い、ソリューションを再ビルドします (増分ビルドでよいでしょう)。
  2. Visual Studio をシャットダウンします。
  3. 更新されたファイル Microsoft.VisualStudio.QualityTools.Samples.MyTestTIP.dll を、ソリューション出力ディレクトリから展開先となった場所にコピーします。既定の展開先は、C:\Program Files\Microsoft Visual Studio 8\Common7\IDE\PrivateAssemblies です。
  4. Visual Studio でソリューションを再度開きます。
  5. 新しいテストを追加します ( MyTest2.mytest など)。

Fuzzer がもたらすもの

効率的な自動化は、ほとんどのソフトウェア品質保証チームの最終的な目標であると思われます。そして、セキュリティ バグを始めとするさまざまな不具合の検出は、特に自動化が困難であることがわかっています。この記事の冒頭で説明した適応性のあるファジー化手法などのヒューリスティックなアプローチを、Visual Studio Team Edition などの自動化フレームワークと組み合わせることで、両者の長所を活かせます。つまり、人間の直感に、単調な繰り返し作業に耐えるコンピュータを加えることで、高いテスト カバレッジを実現できます。

この記事の執筆にご協力いただいたマイクロソフトの Euan Garden と Jeff Wang、そして Personify Design の Juan Perez に感謝します。

Dan Griffin は、ワシントン州シアトル在住のソフトウェア セキュリティ コンサルタントです。Dan はかつてマイクロソフトの Windows セキュリティ開発チームに 7 年間在籍していました。Dan の連絡先は jwsecure.com (英語) です。


編集者のメモ: 極端な方法に陥る理由

編集者のメモ
極端な方法に陥る理由
Howard Dierking

「お使いのシステムは安全ですか」 という質問は、「お使いのシステムはどのくらい安全ですか」という質問と同様に、ビジネスや IT の関係者がこぞってたずねてくる滑稽な質問の 1 つのように思えます。この質問が滑稽なのは、セキュリティが重要ではないからではなく (それどころか、現在セキュリティはかつてないほど重要になってきています)、あまりにも一般的すぎて測定可能な意味を持たないからです。

どちらの質問に対しても、「他よりも安全な面もあれば、安全ではない面もあります」と答えるのがせいぜいです。当然、ビジネスにおいてこのような回答は決して十分とは言えず、実際そのようには回答しません。結局は、かなり極端な絶対的な方法でセキュリティを定義することになります。1 つは、システムのビジネス要件だけに焦点を当て、システムの配置前に実装するセキュリティ関連タスクのチェックリストを IT 部門に作成してもらうという極端な方法です。反対に、これまでソフトウェア業界で行ってきたように、最も厳しい DoD 標準をはるかにしのぐ勢いで、あらゆる攻撃に対する調査とセキュリティ保護に膨大な時間とエネルギーを費す場合もあります。これは、多くの場合、ビジネスのニーズが偏執的なニーズに支配されることになります。

今年の MSDN Magazine のセキュリティの号では、効果的で、測定可能なセキュリティ戦略を策定するための実際的なアプローチに重点が置かれているので非常に満足しています。まず、Michael Howard が、マイクロソフトで安全なソフトウェアの構築に 5 年間携わった経験から学んだベスト プラクティスについて説明しています。Howard のベスト プラクティスの説明に続いて、Michal Chmielewski、Neill Clift、Sergiusz Fonrobert、および Tomasz Ostwald が、セキュリティ関連のアクティビティ (自動化されるものと手動によるものの両方) を既存の開発ライフサイクルにより深く統合するための技法を説明しています。Dan Griffin は、Visual Studio Team System 用のファジー テスト アドインを開発することで、統合の自動化の例を紹介し、より具体的にこの技法を説明しています。最後に、アセンブリ言語なくして終わることができないという開発者の方のために、Adel Abouchaev、Damian Hasse、Scott Lambert、および Greg Wroblewski が、アプリケーションがクラッシュする可能性があるさまざまな状況、およびそれらの各状況によってさらにセキュリティの脆弱性が生じる状況を非常に詳しく解説しています。

今月の多くの記事で繰り返し取り上げられているテーマの 1 つは、セキュリティの問題は絶えず変化し、脅威の現実的な評価に基づいているということです。このため、今月号では、アプリケーションに対して "セキュリティ対策を行う" 方法よりも安定したセキュリティ戦略を策定することに重点が置かれているのです。この点からセキュリティについて考えると、この問題は、静的なインフラストラクチャの問題というよりもビジネス要件のように思えます。そしておそらく、このようにセキュリティをとらえることができれば、妥当で測定可能なセキュリティ戦略の策定方法を見い出し、極端な方法に陥らずに済みます。

測定可能という点について、もう 1 つ簡単に説明します。私たちは常に MSDN Magazine の内容と企画の両方の品質向上に努めています。特に、各記事のオンライン バージョンにある評価システムを信頼しています。ここで読んだ記事のオンライン バージョンで評価システムを使って記事を評価するだけで、MSDN Magazine に大きな影響を与えることができます。皆様のご意見をぜひお寄せください。

コードは安全に! £Howard

Thanks to この記事の執筆に携わったマイクロソフトの技術部門の専門家は、Eric Bidstrup、Joe Binder、Todd Brooks、Claudio Caldato、John Durant、Steve Fox、Matt Gibbs、Ed Hintz、Michael Howard、Richard Johnson、Ivan Medvedev、Dave Reed、Bruce Taimana、Chris Tavares、Mads Torgersen、および James Whittaker です。


ツールボックス: 更新されたグリッド コントロール、オープンソースの比較ツール、その他

ツールボックス
更新されたグリッド コントロール、オープンソースの比較ツール、その他
Scott Mitchell

コンテンツ
Windows フォームのグリッドをアップグレードする
Web サイトに洗練された外観の日付テキスト ボックスを追加する
ファイルをすばやく比較およびマージする
Microsoft ASP.NET AJAX のガイド

Windows フォームのグリッドをアップグレードする

グリッドは、表形式のデータを表示および編集するための最も簡単で直接的な方法であるため、おそらく最もよく使用される高度なユーザー インターフェイスの要素となっています。マイクロソフトは、Visual C++® や Visual Basic® の時代から無償のシンプルなグリッド コントロールを開発者に提供していますが、開発者はサードパーティ ベンダ製のグリッドを利用することもできます。

そうしたサードパーティ ベンダ製の Microsoft® .NET Framework Windows® フォーム グリッド コントロールの 1 つに、FarPoint Technologies, Inc. の Spread for Windows Forms 3 があります。Spread for Windows Forms は、Visual Studio® のツールボックスからフォームにインスタンスをドラッグ アンド ドロップするだけで簡単に .NET アプリケーションに追加できます。初期データは、手動で指定することも、プログラムで指定することも、データバインディングで指定することもできます。

アプリケーションに Spread for Windows Forms のグリッドを追加するのは、Microsoft Excel® のインスタンスを追加するようなものです。Spread for Windows Forms では、Excel と同じように、複数のシートや数式、結合セルや複数の列にまたがるセル、列レベルの並べ替えなどを利用できます。1 行のコードで Excel のスプレッドシートをグリッドにインポートすることもできます。同様に、グリッド データを Excel のスプレッドシートや XML ファイルにエクスポートすることもできます。

また、Spread Designer により、Spread for Windows Forms のグリッドを Visual Studio で簡単にカスタマイズできます。Spread Designer は、Visual Studio のプロパティ ウィンドウから起動できるスタンドアロン プログラムで、グリッドの外観、レイアウト、および書式を指定することができます。

グリッドは、ユーザー インターフェイスのデザインにおいてボタンやラベルと同じくらいよく使用される部品です。Spread for Windows Forms などのサードパーティ製のグリッドを使用すれば、マイクロソフトのグリッドが提供する以上の機能を利用することができます。

価格 : 1 開発者ライセンス - 899 ドル

www.fpoint.com

Spread for Windows Forms のグリッドはカスタマイズできます。
Spread for Windows Forms のグリッドはカスタマイズできます。 (画像を縮小するには、ここをクリックします)
Spread for Windows Forms のグリッドはカスタマイズできます。
Spread for Windows Forms のグリッドはカスタマイズできます。 (画像を拡大するには、ここをクリックします)

Web サイトに洗練された外観の日付テキスト ボックスを追加する

旅行予約サイトなど、ユーザーに日付の入力を求める大規模な商用 Web サイトでは、カレンダーのアイコンが付いたテキスト ボックスがよく使用されています。そのアイコンをクリックすると、現在の月のカレンダーがポップアップ表示されて、目的の日付をクリックするだけで選択することができます。

こうした機能豊富な日付入力ユーザー インターフェイスは、イントラネット サイトや小規模な Web サイトではあまり目にすることはありません。これは、ASP.NET に日付関連の組み込み Web コントロールが含まれていないためでもありますが、さいわいにも、サードパーティが提供するものには日付コントロールが豊富に揃っています。その 1 つが、Peter Blum によって作成された Peter's Date Package (version 2.0.4) です。この製品は、24 の ASP.NET Web コントロールで構成されています。その代表的なコントロールが、カレンダーのアイコンが付いたテキスト ボックスを表示する DateTextBox です。カレンダーのアイコンをクリックすると、カレンダーのインターフェイスがポップアップ表示されます。このポップアップ ウィンドウには、現在の月のカレンダーと、先月や翌月の日付を表示するためのアイコンが表示されます。そのほかに、月と年を選択するためのコントロールも含まれているため、遠い未来や過去の日付もすばやく入力できます。さらに、テキスト ボックスに日付以外の値 (文字や、日付と無関係の区切り記号など) が入力されるのを防ぐクライアント側スクリプトも自動的に生成されます。

Peter's Date Package には、DateTextBox コントロール以外にも、スタンドアロンの月/年選択コントロール、月と日だけを指定できる AnniversaryTextBox コントロール、TimePicker コントロールなどが含まれています。そのほか、一連の日付専用検証コントロールも用意されており、一連の無効な日付を指定できる UnselectableDatesValidator や、2 つの日付コントロールに入力された日付が特定の関係にあること (1 つ目のコントロールの値の方が 2 つ目のコントロールの値より小さいなど) を確認する DateCompareValidator などを使用できます。

何より、Peter's Date Package は使い方が簡単です。新しい Web サイトや既存の Web サイトでこれらの日付コントロールを使用するための構成には 1 分もかかりませんし、ドラッグ アンド ドロップで簡単にこれらの機能豊富な日付コントロールを Web ページに追加できます。作業に行き詰まった場合は、サポート フォーラムに質問を投稿すれば Peter Blum がすぐに答えてくれます。

価格 : 1 Web サーバー ライセンス - 90 ドル

www.peterblum.com/DateControls

DateTextBox は、Peter's Date Package の代表的なコントロールです。
DateTextBox は、Peter's Date Package の代表的なコントロールです。 (画像を縮小するには、ここをクリックします)
DateTextBox は、Peter's Date Package の代表的なコントロールです。
DateTextBox は、Peter's Date Package の代表的なコントロールです。 (画像を拡大するには、ここをクリックします)

ファイルをすばやく比較およびマージする

開発者はアプリケーションのソース コードを一連のファイルとして使用するため、異なるバージョンのファイルやフォルダを比較したり、それらを同期させたりする必要が生じることがよくあります。たとえば、前回のビルドから変更されたファイルを特定する場合や、ステージング サーバーに対して行われた変更で運用 Web サイトのページを更新する場合などが考えられます。ファイルの相違点を見つけるプログラムは昔からありますし (Windows には以前から WinDiff というプログラムが含まれています)、ほとんどのソース コード管理プロバイダには、ソース管理下にあるファイルのバージョンを簡単に比較できるように WinDiff のような種類のプログラムがバンドルされていますが、それ以上の機能を求める開発者もいます。

WinMerge (version 2.6.8) は、無償、高速、多機能なオープンソースのファイル/フォルダの比較/同期ツールです。WinMerge を起動すると、比較する 2 つのファイルまたはフォルダを選択するように求められます。このとき、ファイル フィルタや行フィルタを指定することもできます。ファイル フィルタは、フォルダ (またはサブフォルダ) 内の一連のファイルを含める場合に、含めるファイルと除外するファイルを指定します。たとえば Web 開発者なら、HTML や ASP.NET の Web ページを含めて、GIF、JPG、CSS、JavaScript などのファイルを除外するフィルタを使用できます。ファイル フィルタは、自分で作成することも、WinMerge に同梱されているものを使用することもできます。一方、行フィルタを使用すると、特定の正規表現に一致するテキスト行を比較から除外できます。

2 つのフォルダのファイルを比較すると、各フォルダのファイルの一覧が表示されて、同じファイルかどうかが示されます。一覧のファイルをクリックすると、2 つのファイルの内容を表示するウィンドウが開いて、相違点が色で示されます。

WinMerge では、ファイルの同期化も簡単です。一度に 1 つずつ相違点を確認し、左のファイルを右のファイルに合わせるか、右のファイルを左のファイルに合わせるかを選択できます。また、ボタンを 1 つクリックするだけで一方のファイルをもう一方とまったく同じにすることもできます。

WinMerge には直感的なグラフィカル ユーザー インターフェイスが用意されていますが、コマンド ラインから実行したり、Windows シェルに統合したりすることもできます。

価格 : 無料

www.winmerge.org

WinMerge には直感的なグラフィカル インターフェイスが用意されています。
WinMerge には直感的なグラフィカル インターフェイスが用意されています。 (画像を縮小するには、ここをクリックします)
WinMerge には直感的なグラフィカル インターフェイスが用意されています。
WinMerge には直感的なグラフィカル インターフェイスが用意されています。 (画像を拡大するには、ここをクリックします)

Microsoft ASP.NET AJAX のガイド

AJAX は、デスクトップ アプリケーションのような洗練されたユーザー エクスペリエンスを提供するインタラクティブな Web サイトを構築するために広く利用されています。AJAX 対応のサイトは、JavaScript を使用して Web サーバーと非同期に通信し、ブラウザのインターフェイスをシームレスに更新します。サードパーティやオープンソースのさまざまな AJAX フレームワークを利用することができますが、マイクロソフトからも独自の AJAX フレームワークとして ASP.NET AJAX (ajax.asp.net) がリリースされました。

AJAX 関連のほとんどの書籍では、関連技術の初歩レベルの詳細が余すところなく説明されています。しかし、AJAX 対応 ASP.NET アプリケーションの構築をすぐに始めたい開発者には、そこまで詳しい説明は必要ありません。そのような開発者にお勧めしたいのが、Matt Gibbs と Dan Wahlin の『Professional ASP.NET 2.0 AJAX』 (Wrox Press、2007 年) です。この本は、AJAX の基本的な概念 (非同期ポストバック、ブラウザのドキュメント オブジェクト モデル、スクリプトによる HTTP 要求の送信など) に焦点を絞り、それぞれの概念を ASP.NET 開発者におなじみの用語で説明しています。また、Microsoft ASP.NET AJAX ライブラリや、UpdatePanel コントロール、ASP.NET クライアント ライブラリ、ScriptManager、および AJAX Toolkit のコントロールの使用についても取り上げられています。

そのほか、デバッグのヒントやテクニックも紹介されています。AJAX 対応 Web アプリケーションは、構築に使用されるテクニックのためにテスト プロセスが複雑になります。AJAX 関連の関数の多くは Web ブラウザで実行されるため、サーバー側コードの豊富なデバッグ サポートを利用できません。この本のテストについての章では、ASP.NET AJAX のデバッグ関連のクラスや、マイクロソフトの Script Debugger、Eric Lawrence の Fiddler、Nikhil Kothari の Web Development Helper などの便利なツールが紹介されています。さらに、AJAX 対応 ASP.NET アプリケーションの配置に伴う課題も取り上げられています。

価格 : 39.99 ドル

www.wrox.com

ご意見やご質問は、

Scott toolsmm@microsoft.com.

までお送りください。
Scott Mitchell は、多数の著書を出版し、4GuysFromRolla.com の創設者でもあります。また、1998 年からは Microsoft MVP として Microsoft Web テクノロジに携わっています。現在は、フリーのコンサルタント、トレーナー、およびライターとして活躍しています。Scott の連絡先は、Mitchell@4guysfromrolla.com です。また、ScottOnWriting.NET (英語) にブログを公開しています。


CLR 徹底解剖: オブジェクトの有効期間を管理する

CLR 徹底解剖
オブジェクトの有効期間を管理する
Tim Fischer

コンテンツ
ガベージ コレクションとファイナライズ
COM での有効期間管理
.NET の確定的なファイナライズ
COM ライブラリのファイナライズ
メモリ リーク

Microsoft® .NET Framework ではマネージ実行環境を利用できますが、リソースを慎重に使用し、有効期間の管理について考慮し、これらの問題に適切に対処するコードを記述することが重要です。このコラムでは、.NET または COM クラスを使用している開発者が、有効期間の管理に関する理解を深め、リソースの落とし穴に陥る可能性がある状況を認識し、これらの問題の解決方法を把握することができるようにします。

わかりやすくするために、このコラムで示すサンプル コードは、シングル スレッド アプリケーションでのみ動作するようになっています。.NET リソース管理用の再利用できるスレッド セーフなコードは、CodePlex の LifeTimeScope.net プロジェクトに含まれています (codeplex.com/lifetimescope を参照)。

.NET プログラミングでは、オブジェクトは新しいキーワードを使用して作成され、マネージ ヒープに割り当てられます。オブジェクトは、強参照のパスを介して強いルート参照からアクセスできなくなったとガベージ コレクタによって判断されるまで有効です。各プログラムには、メイン メソッドと関連クラス、およびローカル変数、静的メンバ、またはイベントを含む可能性がある静的クラスがあります。これらの静的参照またはローカル参照は、プログラム内のルート参照と見なされます (図 1 を参照)。一般的な .NET Framework 参照は、強参照であることが知られています。オブジェクトの有効期間は、ルート参照からオブジェクトを指す強参照のパスによって決まります。

図 1 ヒープ上のオブジェクトへの強参照
図 1 ヒープ上のオブジェクトへの強参照

高速なキャッシュまたはハッシュをオブジェクト グラフに構築する場合などは、オブジェクトの有効期間を変更しないオブジェクトへの参照を追加するとおもしろいかもしれません。このために、.NET Framework では、オブジェクト間の弱参照を実装できる WeakReference という追加のクラスが用意されています。この弱参照は、オブジェクトの有効期間の決定時に無視されます。

イベントも強いルート参照である場合があり、強参照のパスに影響する可能性があるので、オブジェクトの有効期間に影響します。共通言語ランタイム (CLR) 2.0 の一般的なイベントは、イベント ソースとリスナ間の強い双方向参照で、既に終了しているはずのオブジェクト (ソースまたはリスナ) を有効なままにすることができます。このため、WeakEvent クラスが .NET Framework 3.0 に追加されています。WeakEvent パターンはまだあまりよく知られていませんが、Observer パターンを正常に実装するために必要なので、このパターンの理解を深めることが重要です。WeakEvent パターンは、データ バインドによるリークを防ぐために Windows® Presentation Foundation (WPF) のデータ バインド実装で使用されています。

ガベージ コレクションとファイナライズ

オブジェクトがまだ有効かどうか、およびオブジェクトが終了するとどうなるかを CLR が決定する方法について説明します。2000 年 11 月号の MSDN® Magazine (msdn.microsoft.com/msdnmag/issues/1100/GCI) で、Jeffrey Richter が手順について説明しています。各 .NET プロセスによって、ガベージ コレクタで使用される個別のスレッドが実行されます。ガベージ コレクタの実行時に、他のすべてのスレッドが一時停止します。ガベージ コレクタによってメモリ構造および参照が分析され、終了しているオブジェクトが検出されます。このようなオブジェクトは、ファイナライズというプロセスのキューに登録されます。第 2 フェーズで、ファイナライズ プロセスがガベージ コレクタで使用される個別のスレッドに対して実行され、Finalizer メソッドが各オブジェクトで呼び出されます。

このプロセスには 2 つの重要なポイントがあります。1 点目は、ファイナライザはオブジェクトを作成したスレッドとは異なるスレッドから呼び出されるという点です。2 点目は、ファイナライザは必ずしもオブジェクトが実際に終了した時点に近いタイミングで呼び出されるとは限らないという点です (これは、システムに負荷がかかっているがメモリは使用可能である場合に顕著です)。

ガベージ コレクタは、System.GC 静的クラスで公開されます。GC.Collect を呼び出すと、ガベージ コレクションを強制的に実行し、終了したオブジェクトを特定してファイナライズのキューに登録することができます。また、GC.WaitForPendingFinalizers を呼び出すと、キューに登録されているすべてのオブジェクトに対してファイナライズを強制的に実行することができます。ただし、WaitForPendingFinalizers の呼び出しに関しては 2 つの問題点があります。時間がかかるプロセスであるという点と、ファイナライザが別のスレッドから呼び出されるという点です。このため、システム リソースへの参照を解放する必要があり、ファイナライザでこの操作を実行する場合、一部のアンマネージ リソースがスレッドに依存しているときなどは、パフォーマンスとスレッド化に関する問題が発生する可能性があります (この場合は、Dispose パターンについて学習してください)。

.NET Framework チームは、オブジェクトをタイムリーに破棄する必要があること、および破棄コードを特定のスレッドに対して実行できるようにする必要があることを既に認めているので、Dispose パターンと呼ばれるソリューションを打ち出しました。詳細については、MSDN Magazine 2007 年 7 月号の「CLR 徹底解剖」コラム (msdn.microsoft.com/msdnmag/issues/07/07/CLRInsideOut) を参照してください。

Dispose パターンは、リソースを保持しておりタイムリーに破棄される必要があるすべてのオブジェクトで実装する必要がある規則です。これは、IDisposable インターフェイスを実装し、そのインターフェイスによって公開される Dispose メソッドの実装でリソースを解放することで実現します。ただし、アンマネージ環境と同様に、Dispose メソッドがクライアント コードで適切なタイミングで呼び出されるようにするのはプログラマの役目です。Brian Harry は、チームがこのソリューションを打ち出した理由とその他の興味深いソリューションの適用が成功しなかった理由に関する説明をパブリック フォーラムに投稿しています (discuss.develop.com/archives/wa.exe?A2=ind0010A&L=DOTNET&P=R28572 を参照)。Dispose パターンは、リソースを所有しているすべてのオブジェクトで適切に実装する必要があります。

COM での有効期間管理
有効期間管理に関するリソース
  • MSDN .NET Framework Developer Center
  • WeakEvent パターン
  • LifeTimeScope.net ヘルパ クラス

COM 環境では、有効期間管理は参照カウントを使用して行われます。各 COM オブジェクトでは、そのオブジェクトを指すクライアント参照が内部でカウントされます。このようなカウンタは、クライアント開発者が IUnknown::AddRef および IUnknown::Release を呼び出すことによって増減します。AddRef はコンポーネントへの参照が設定されたときに呼び出す必要があり、これによってカウンタの値が増加します。一方、Release は参照が破棄されたときに呼び出す必要があり、これによってカウンタの値が減少します。カウンタが 0 に戻ると、オブジェクトを破棄できるようになります。

AddRef および Release は、Active Template Library (ATL) の SmartPointers を使用するか、Visual Basic® 6.0 などのコンパイラやランタイムで必要なコードを生成すると簡単に呼び出せていました。イベントは、一方向への強いポインタと別方向への弱いポインタだけでした。

参照カウントに関する主な 2 つの問題点は、パフォーマンスが低下するという点と、循環参照シナリオに含まれる終了したオブジェクトを検出できない可能性があるという点です。COM は通常、大規模なコンポーネントを組み合わせるためのテクノロジとして使用されるので、COM では一般的に、パフォーマンスの低下は許容できます。ただし、.NET Framework はクラス レベルで使用されるので、参照カウントが CLR に追加されると、パフォーマンスへの影響は多大なものになります。

マネージ コードから COM コンポーネントを使用する場合、tlbimp.exe を使用していわゆるランタイム呼び出し可能ラッパー (RCW) を生成します。RCW によって COM コンポーネントの機能がマネージ環境に公開されます。なお、tlbimp.exe は、Visual Studio® で COM コンポーネントを参照するとバックグラウンドで実行されます。RCW はデバッガで System.__ComObject と表示されるので、簡単に識別できます。RCW は、COM オブジェクトへのアンマネージ ポインタを 1 つ保持し、オブジェクトで AddRef および Release を呼び出す .NET 型です。AddRef はコンポーネントが作成されると呼び出され、Release は RCW がファイナライズされると呼び出されます。

しかし、RCW はいつファイナライズされるのでしょうか。これはわからないので、RCW で Dispose を呼び出すメソッドが必要です。残念ながら、生成した RCW では IDisposable は実装されません。現時点で、不足している Dispose メソッドの呼び出しと同等の呼び出しは次のとおりです。

System.Runtime.InteropServices.Marshal.ReleaseComObject(MyCOMObject)

.NET の確定的なファイナライズ

多くの場合、使用するリソースができるだけ早く解放されるようにする必要があります。理想的な方法は、C# で using キーワードを使用して、プログラム フローが中かっこで示されたスコープから外れるとオブジェクトで Dispose メソッドが呼び出されるようにすることです。

using (FileStream theFileStream = File.Create("C:\\hello.txt"))
{
    string s = theFileStream.Name;
}

また、メソッドの終了時にローカル変数のデストラクタが C++/CLI によって自動的に呼び出されるということにも注意してください。これらのシナリオでは通常、Dispose メソッドはブロックまたはメソッドの最後に自動的に呼び出されると想定できます。

ただし、リソースの使用スコープがローカルではない場合、これらの方法は使用できません。FileStream オブジェクトを入力パラメータとして使用する別のメソッドを前述のコードで呼び出すとします。そのメソッドでは、Dispose が呼び出されません (using キーワードが使用されません)。スレッド間でリソースを共有するマルチスレッド サーバーのコンテキストでは、これはさらに複雑になる可能性があります。

残念ながら、C# には、スコープ間またはスレッド間で使用できる using キーワードと同等の簡単なコンストラクタはありません。このため、Dispose の呼び出しをコンポーネント間でネゴシエートして 1 回だけ適切なタイミングで実行されるようにする必要があるので、コードとルーチンの再利用は困難です。

Dispose を明示的に呼び出さない場合、Dispose はファイナライザによって呼び出され、オブジェクトで Dispose パターンが適切に実装されているかどうかが確認されます。ただし、ファイナライザがいつ呼び出されるかがわからないので、負荷が高い環境ではリソースが不足する可能性があります。また、オブジェクトを作成したスレッドとは異なるスレッドから呼び出されると、ファイナライザは失敗する可能性があります。これは、ファイナライザでアンマネージ リソースまたは COM リソースを解放しようとする場合に問題になることがあります。

この問題の解決策としては、IDisposable を実装するあらゆる型の汎用ラッパー クラスを作成することが考えられます。ラッパー クラスによって IDisposable インターフェイスが返されますが、コンポーネントの呼び出しをデリゲートする前に参照カウンタが内部的に設定されます。図 2 に示すコードでは、AddRef という新しい拡張メソッドを使用して、特定のオブジェクトの内部参照カウンタを含む対応するラッパー オブジェクトを取得します。

Figure 2 IDisposable ラッパー (Close Figure 2)
static void Main(string[] args)
{
    FileStream theFileStream = File.Create("C:\\hello.txt");
    // Unscoped use of RefCounted
    using (theFileStream.AddRef())
    {
        a(theFileStream);
    }

    System.Console.Read();
}

static void a(FileStream inFileStream)
{
    // Unscoped use of RefCounted
    using (inFileStream.AddRef())
    {
        // do something
        string s = inFileStream.Name;
    }
}

AddRef が呼び出されるとハッシュ テーブルが調べられ、オブジェクトに既にラッパーがあるかどうかが確認されます。ラッパーがある場合、そのラッパーが返されてカウンタの値が増加します。ラッパーがない場合、新しいラッパーが作成されてハッシュ テーブルに追加されます。プログラムが using ブロックの末尾に達すると、Dispose が自動的に呼び出されてカウンタの値が減少します。カウンタがゼロになると、Dispose の呼び出しが基になるオブジェクトに伝達されます。

拡張メソッドとラッパーの実装を図 3 に示します。わかりやすくするために、このコードはスレッド セーフに設計されていません。

Figure 3 AddRef の拡張とラッパー (Close Figure 3)
public static class ReferenceCountingExtensions
{
    public static System.Collections.Generic.Dictionary<object,object>
        theHashOfReferenceCountedClasses =
        new System.Collections.Generic.Dictionary<object,object>();
    public static RefCounted<T> AddRef<T>(this T resource)
        where T : IDisposable
    {
        object theObject=null;
        theHashOfReferenceCountedClasses.TryGetValue(resource,
            out theObject);
        if (theObject == null)
        {
            theObject = new RefCounted<T>(resource);
            theHashOfReferenceCountedClasses[resource] = theObject;
        }
        else
        {
            ((RefCounted<T>)theObject).AddRef();
        }
        return (RefCounted<T>) theObject;
    }
}

// Base Class for a RefCountedUnmanagedResourceProtectingClass
public class RefCounted<T> : IDisposable
{
    private T theResource;
    bool IsFinalDisposed = false;
    int RefCounter = 0;
    public RefCounted(T inResource)
    {
        theResource = inResource;
        System.Threading.Interlocked.Increment(ref RefCounter);
    }
    public RefCounted<T> AddRef()
    {
        System.Threading.Interlocked.Increment(ref RefCounter);
        return this;
    }
    public T Resource
    {
        get
        {
            return theResource;
        }
    }
    ~RefCounted()
    {
        if (!IsFinalDisposed)
        {
            Dispose();
        }
    }
     public void Dispose()
    {
        System.Threading.Interlocked.Decrement(ref RefCounter);
        if ((RefCounter == 0) && (!IsFinalDisposed)) FinalDispose();
    }
    virtual public void FinalDispose()
    {
        ((IDisposable)theResource).Dispose();
        IsFinalDisposed = true;
    }
}

ファイナライザが最後まで実行されることを確認してください。.NET Framework ランタイムには、制約された実行領域という概念とこの概念の実現に役立つ CriticalFinalizerObject があります。

COM ライブラリのファイナライズ

C# から COM オブジェクト モデルを使用するシナリオについて説明します。COM コンポーネントは、その登録済みのタイプ ライブラリへの参照を作成することによって簡単に使用できます。Telephony API (TAPI) を公開する COM コンポーネントを次のコード行で使用するとします。ここで、有効期間管理に関連するバグを探してください。

void foo (CTAPIApplication TAPIApplication){
    string s = TAPIApplication.Ports[0].SpeechListener.GetName();
}

バグは、Ports コレクションがオブジェクト モデルから取得されたが、その ReleaseComObject メソッドが呼び出されていないことです。SpeechListener についても同じことが言えます。foo によって ComObject が解放されると考えるのは安全ではありません。何によっていつオブジェクトが解放されるかは、このコードからはわかりません。それでは、どうしたらよいでしょうか。

この場合も、解決策は AddRef メソッドを使用することですが、ComObject の場合に ReleaseComObject を呼び出す必要があることを認識するように RefCounted クラスを変更する必要があります。図 4 に、拡張された FinalDispose メソッドを示します。また、IDisposable および COM オブジェクトで共有する共通インターフェイスがないので、AddRef<T> の呼び出しから where T : IDisposable が削除されています。

Figure 4 FinalDispose メソッド (Close Figure 4)
virtual public void FinalDispose()
{
    if(theResource.GetType().IsCOMObject)
    {
        Marshal.ReleaseComObject(theResource);
    }

    else
    {
        ((IDisposable)theResource).Dispose();
    }

    IsFinalDisposed = true;
}

public static RefCounted<T> AddRef<T>(this T resource)

これで、図 5 に示すように、Telephony API の呼び出しを書き直すことができます。このコードもあまり便利ではありませんが、ある時点で例外がスローされても using ステートメントによって ReleaseComObject が実際に呼び出されるようになるので非常に安全です。LifeTimeScope という別のヘルパ クラスを追加して AddRef の戻り値の型を変更すると、コードがさらに読みやすくなります。

Figure 5 COM を呼び出す (Close Figure 5)
void foo(CTAPIApplication aTAPIApplication)
{
    using(PortsCollection aPortsCollection =
        aTAPIApplication.Ports.AddRef())
    {
        using (Port aPort = aPortsCollection[0].AddRef())
        {
            using(SpeechListener aListener =
                aPort.SpeechListener.AddRef())
            {
                string s  = aListener.GetName()
            }
        }
    }
}

void foo (CTAPIApplication TAPIApplication)
{
    using (new LifeTimeScope())
    {
        string s =
            TAPIApplication.Ports.AddRef()[0].AddRef()
            .SpeechListener.AddRef().GetName();
    }
}

図 6 に示す LifeTimeScope クラスは、using ブロック内の AddRef がアクセスしたすべてのオブジェクト ラッパーで Dispose を呼び出すヘルパとして機能します。AddRef も若干変更されています (図 7 を参照)。変更はすべて適切ですが、AddRef を自分で呼び出す必要があります。

Figure 7 修正後の AddRef (Close Figure 7)
public static class ReferenceCountingExtensions
{
    static public System.Collections.Generic.Dictionary<object, object>
        theHashOfReferenceCountedClasses =
        new System.Collections.Generic.Dictionary<object, object>();

    public static T AddRef<T>(this T resource)
    {
        object theObject=null;
        theHashOfReferenceCountedClasses.TryGetValue(resource,
            out theObject);
        if (theObject == null)
        {
            theObject = new RefCounted<T>(resource);
            theHashOfReferenceCountedClasses[resource] = theObject;
        }
        else
        {
            ((RefCounted<T>)theObject).AddRef();
        }
        System.LifeTimeScope.Current
            .theListOfWrapperObjectsToCallDisposeOn.Add(theObject);
        return (T) resource;
    }
}
Figure 6 LifeTimeScope (Close Figure 6)
public  class LifeTimeScope : IDisposable
{
    private static LifeTimeScope _Current = null;

    private static Stack<LifeTimeScope> theScopeStack =
        new Stack<LifeTimeScope>();

    public static LifeTimeScope Current
    {
        get{ return _Current; }
    }

    public System.Collections.ArrayList
        theListOfWrapperObjectsToCallDisposeOn =
        new System.Collections.ArrayList();

    public LifeTimeScope()
    {
        _Current = this;
        theScopeStack.Push(this);
    }

    public void Dispose()
    {
        foreach (IDisposable theObject in
            theListOfWrapperObjectsToCallDisposeOn)
        {
            theObject.Dispose();
        }
        _Current = theScopeStack.Pop();
    }
}

メモリ リーク

最も一般的な問題は、イベントのサブスクリプションのためにアプリケーションでメモリ リークが発生することです。たとえば、コンストラクタで NetworkChange イベントをサブスクライブする Windows フォーム ユーザー コントロールを次のように記述できます。

NetworkChange.NetworkAvailabilityChanged += new
    NetworkAvailabilityChangedEventHandler(ctrl_NetAvailChangedHandler);

ユーザー コントロールと NetworkAvailabilityChanged 静的クラスは、双方向で相互への強い参照を保持しています。NetworkAvailabilityChanged は静的なルート参照なので、ユーザー コントロールはイベント ハンドラが削除されるまでメモリから解放されません。

参照先オブジェクトの有効期間を超えるオブジェクト (この場合は NetworkAvailabilityChanged とその参照先ユーザー コントロール) でイベントのサブスクライブを解除しない場合、自動的にメモリ リークを発生させることになります。この問題は一般的で、データ バインドなどのあらゆる Observer パターン シナリオに当てはまります。

.NET Framework 2.0 のドキュメントに、オブジェクトの Dispose メソッドでイベントのサブスクライブを解除する必要があることが記載されています。また、確実に Dispose が呼び出されるようにすることもお勧めします。これは、.NET Framework 2.0 環境では、イベントは考慮対象となるリソースであるためです。ただし、.NET Framework 3.0 では、WeakEvent パターンに基づくソリューションを使用できます。

最後に、アンマネージ リソースを確実に破棄するには、GC.Collect をオンデマンドで実行できるようにする HandleCollector クラスを使用する方法もあります。図 8 に、ハンドル名 (文字列)、初期しきい値 (整数)、および最大しきい値 (整数) の 3 つのパラメータを指定して HandleCollector のインスタンスを作成する例を示します。初期しきい値は、ガベージ コレクタがガベージ コレクションの実行を開始できる時点です。最大しきい値は、ガベージ コレクタがガベージ コレクションを実行する必要がある時点です。

Figure 8 HandleCollector を使用する (Close Figure 8)
// Handle Collector offers an easy way to have garbage collection
// started once we have 30+ active (and possibly destroyed) objects

class MyClassThatForcesGarbageCollectionIf30HandlesAreActive
{
    // Create a new HandleCollector
    static readonly HandleCollector appHandleCollector =
        new HandleCollector("ApplicationUnmanagedHandles", 5, 30);

    public MyClassThatForcesGarbageCollectionIf30HandlesAreActive()
    {
        // Increment Handle Count
        appHandleCollector.Add();
    }
    ~MyClassThatForcesGarbageCollectionIf30HandlesAreActive()
    {
        // Decrement Handle Count
        appHandleCollector.Remove();
    }
}
}

これで、.NET または COM クラスでの有効期間管理の概要説明を終わります。その問題について認識し、解決できるようになっていただけたならさいわいです。詳細については、「有効期間管理に関するリソース」のリンクを参照してください。

ご意見やご質問は、

こちら clrinout@microsoft.com.

まで英語でお送りください。
Tim Fischer は、ドイツ在住のマイクロソフトの Visual Studio および Expression 製品のプログラム マネージャです。シュトゥットガルト大学でコンピュータ サイエンスの学位を取得し、モデル駆動型開発のコンサルタントとして 12 年間勤務していました。また、世界的な .NET ソフトウェア ファクトリの構築を専門とする会社である tangible engineering の創立者でもあります。


基本的な本能: 拡張メソッド

基本的な本能
拡張メソッド
Adrian "Spotty" Bowles

コンテンツ
拡張メソッドの作成と使用
拡張メソッドの利点
セキュリティの問題
拡張メソッドによるオーバーロード
型の選択
Object 型の拡張
.NET Framework 2.0 アプリケーションの拡張メソッド

従来のオブジェクト指向開発では、クラスの拡張は通常、基本クラスからクラスを派生させて、その派生クラスの機能を強化することによって行われます。Visual Basic® ではこうしたオブジェクト指向の概念も引き続きサポートされていますが、クラスが NotInheritable としてマークされていて、継承による動作の変更が不可能な場合もあります。その場合、それらのクラスをカスタマイズする方法はありません。たとえば、System.String クラスはそうしたクラスの 1 つです。

しかし、Visual Basic 2008 で導入された新機能の拡張メソッドを使用すれば、NotInheritable の型も含め、すべての既存の型の機能を拡張することができます。これらの拡張メソッドは、LINQ の実装において非常に重要な役割を担っています。

既存のコードを壊さずに更新するのが難しい既存の型はたくさんあります。たとえば、IEnumerable(Of T) インターフェイスはその 1 つです。LINQ をサポートするには、このインターフェイスに新しいメソッドを追加しなければなりませんが、新しいメソッドを追加してこのインターフェイスを変更すると、既存のコンシューマとの互換性が損なわれてしまいます。新しいインターフェイスを追加することも考えられますが、既存の IEnumerable(Of T) インターフェイスを補足するために新しいインターフェイスを作成するのは、スマートな設計とは言えません。

要するに、既存の型を、既存のコントラクトを変更せずに新しい機能で拡張するための手段が必要なのです。

拡張メソッドは、システムの型 (値型、参照型、およびインターフェイス型) を新しいメソッドで拡張するための単純なメカニズムを提供します。これらのメソッドを使用すると、元の型を拡張し、定義された標準のインスタンス メソッドのように呼び出すことができますが、元の型やそのメソッドには手は加えられません。拡張メソッドは本当の型で定義されているように見えますが、実際には、元の型は一切変更されません。

これは、標準のオブジェクト指向の概念ではありません。Microsoft® .NET Framework の実装に固有の機能です。この機能によってさまざまなことが可能になりますが、注目すべき点は、コンパイラによって生成される基になる中間言語 (IL) コードでは実際には共有メソッドの呼び出しが行われるだけで、.NET Framework 3.5 に固有の新しいことは何も行われないということです。

つまり、この Visual Basic 2008 の機能を、.NET Framework の以前のバージョンをターゲットにして使用できることになります。この機能では、拡張する対象の型は変更されず、実際には以前のバージョンの Framework でも可能なことしか行われないため、新たなセキュリティの問題が導入されることもありません。

拡張メソッドの作成と使用

図 1 のコードは、大文字と小文字を入れ替えた文字列を返す AlternateCase 拡張メソッドを示しています。この拡張メソッドは、先に述べたように NotInheritable としてマークされている、System.String クラスを拡張します。

Figure 1 AlternateCase 拡張メソッド (Close Figure 1)
Imports System.Runtime.CompilerServices

Module Module1
    Sub Main()
        Dim s As String = "Hello World"

'Call AlternateCase method as an extension member on
'variable s
        Console.WriteLine(s.AlternateCase)

'Invoke if it was not an extension method or
'resolve conflicts
Console.WriteLine(Extensions.AlternateCase(s))

    End Sub
End Module

Module Extensions
<Extension()> _
Function AlternateCase(ByVal x As String) As String
        Dim bCase = False
        Dim SB as new System.Text.StringBuilder
        For Each c In x
            If bCase Then
                SB.Append(c.ToUpper)
            Else
                SB.Append(c.ToLower)
            End If
            bCase = Not bCase
        Next
        Return SB.ToString
    End Function
End Module

では、この例を順番に見ていきましょう。メソッド宣言には、このメソッドを拡張メソッドとして識別する属性が含まれています。拡張メソッドは、System.Core.Dll に含まれている System.Runtime.CompilerServices 名前空間の拡張属性を適用することによって定義されます。ご存じない方のために一応説明しておくと、属性は、コンパイラが追加機能をトリガするために使用するメタデータを提供するために使用されます。この場合は、拡張属性により、拡張メソッドへの最初の引数として指定された型 (この例では String) でそのメソッドを呼び出すことができるという情報がコンパイラに伝えられます。

Visual Basic 言語では、すべての拡張メソッド宣言をモジュール内に配置することになっています。また、拡張メソッドはその名のとおり、サブルーチンや関数として作成することはできますが、それ以外のもの (プロパティやイベントなど) として宣言することは禁止されています。拡張できる型には制限はありません。任意のクラス、構造体、インターフェイスを拡張できます。

拡張メソッドがモジュールに制限されているのは、簡単に使用できるようにするためです。これは、名前空間のすべてのメソッドをスコープに含める Imports ステートメントを使用すると、新しい API が Visual Basic プログラムのスコープに含まれるようにするための設計です。

Visual Basic のメソッドはすべて型 (クラス、構造体、モジュール、またはその他の型) の内部で宣言されている必要があります。拡張メソッドも例外ではありません。ただし、拡張する型の外部の型で定義されるという点で多少異なります。このことは、拡張メソッドを複数のモジュールや名前空間から使用する際に名前の競合の原因になる可能性があります。

拡張メソッドの宣言をモジュールに制限することで、設計が大幅に単純化されます。というのも、モジュールは継承チェーンに参加できませんし、部分モジュールという概念もないからです。名前空間やモジュールを含む Imports ステートメントを使用すると、それらの共有メソッドがスコープに含まれて、完全修飾名を使用せずに呼び出せるようになります。

たとえば、Module Extensions を次のように別の名前空間に配置したとします。

Namespace NewFeatures
Module Extensions
<Extension()> Function AlternateCase(ByVal x As String) As String
        Dim bCase = False
        Dim SB as new System.Text.StringBuilder
        For Each c In x
            If bCase Then
                SB.Append(c.ToUpper)
            Else
                SB.Append(c.ToLower)
            End If
            bCase = Not bCase
        Next
        Return SB.ToString
End Module
End Namespace

この名前空間内の拡張メソッド宣言をスコープに含めるには、次のステートメントを使用します。

Imports NewFeatures
Imports System.Runtime.CompilerServices

Module Module1
    Sub Main()
        Dim s As String = "Hello World"

'Call AlternateCase method as an extension member on 'variable s
        Console.WriteLine(s.AlternateCase)

'Invoking if it was not an extension method or
'resolve conflicts
Console.WriteLine(Extensions.AlternateCase(s))
    End Sub
End Module

拡張メソッドは Visual Basic に固有のものではありません。C# の実装も同様で、C# では拡張メソッドを静的クラスで宣言しなければなりません。これにより、モジュールと同じような機能が実現されます。C# と Visual Basic の両方を使用している場合、拡張メソッドでは相互運用性が確保されているため、一方の言語で拡張メソッドを作成し、もう一方の言語で使用することができます。

プロジェクトを以前のバージョンからアップグレードした場合は、ターゲット Framework をバージョン 3.5 にアップグレードし、System.Core.Dll への参照を手動で追加する必要があります。Framework ターゲットをアップグレードしたくない場合は、ExtensionAttribute を再作成することによって拡張メソッドを利用することもできます。これについては後ほど詳しく説明します。そのほか、System.Runtime.CompilerServices 名前空間 (ExtensionAttribute を含む名前空間) をインポートする必要もあります。インポートしない場合は、この属性を使用するときに完全修飾名を使用する必要があります。

この例の最後の部分は呼び出しです。改良された IntelliSense® により、すべての既存インスタンス メソッドと共に拡張メソッドが表示されます。拡張メソッドには、インスタンス メソッドと区別するために少し異なるアイコンが使用されています (青の下向き矢印。図 2 を参照)。

図 2 拡張メソッドとインスタンス メソッドのアイコン
図 2 拡張メソッドとインスタンス メソッドのアイコン(画像を縮小するには、ここをクリックします)
図 2 拡張メソッドとインスタンス メソッドのアイコン
図 2 拡張メソッドとインスタンス メソッドのアイコン (画像を拡大するには、ここをクリックします)

"定義へ移動" や "名前の変更" などの Visual Studio® IDE の機能は、通常のインスタンス メソッドと同様に拡張メソッドでも使用できます。シグネチャを表示する際のツールヒントにも拡張属性が表示されます。

IntelliSense にその他の拡張メソッド (First や FirstorDefault など) が含まれていることに気付いた方もいるかもしれません。これらは、LINQ の一部としてあらかじめ用意されている拡張メソッドの一部で、.NET Framework 3.5 の既定の参照に含まれています。これらの拡張メソッドは、ジェネリック インターフェイス System.Collections.Generic.IEnumerable(Of T) を System.Linq 名前空間から拡張しています。

LINQ の機能は、バージョン 3.5 のプロジェクトでは既定で有効になります。したがって、System.Core.Dll で定義されているすべての拡張メソッドを使用できます。これを確認するには、既定のアセンブリ参照と、インポートされた名前空間を表示します ([プロジェクトのプロパティ] | [参照] | [インポートされた名前空間] に移動すると、System.Linq のチェック ボックスが既にオンになっています)。ファイル レベルの Imports ステートメント (Imports System.Linq) を使用してこれらのメソッドをスコープに含めることもできます。

拡張メソッドの利点

拡張メソッドを使用すると、変更したくない型に機能を追加することができるため、既存のアプリケーションのコードが破損するリスクを回避できます。既存のクラス ライブラリを物理的に変更することなく、標準のインターフェイスを追加のメソッドで拡張できます。.NET 型や古い COM/ActiveX® コントロール型を新しいコードで拡張しても、それらの型を使用する古いアプリケーションが破損するおそれはありません。

拡張メソッドが導入される前は、クラスやインターフェイスに機能を追加するには次の 3 つの選択肢がありました。

  • ソース コードに機能を追加する。ただし、そのためにはソース コードにアクセスできる必要がありますが、それは必ずしも可能とは限りません。
  • 継承を使用して、一方の型に含まれている機能をもう一方の新しい派生型に継承する。ただし、すべての型を継承できるわけではありません。
  • 機能をゼロから実装し直す。

これらの方法では、いずれもサードパーティの型の拡張が非常に困難です。ソース コードを入手してコンポーネントを再コンパイルできたとしても、そのコンポーネントを使用する既存のアプリケーションが破損しないように元のコンポーネントとの互換性を維持する必要もあります。新しい派生クラスを作成したり、機能をゼロから実装し直したりする場合は、その新しいコンポーネントを使用するようにすべての既存アプリケーションを修正するか、元の機能と更新された新しい機能との間の分岐を作成する必要があります (この場合はそのコンポーネントの 2 つのバージョンが必要になります)。

これらのソリューションは、いずれも理想的なものではありませんでした。拡張メソッドでは、単純に既存のクラスに機能を追加することができます。元のバイナリは一切変更されないため、そのバイナリを使用する既存のアプリケーションはすべて引き続き機能しますが、アプリケーションの機能を将来的に拡張できるようになっています。

パフォーマンスにもさほど影響はありません。Visual Basic のソース コードは違って見えるかもしれませんが、そのコードは単純な共有メソッドの呼び出しにコンパイルされ、共有メソッドの呼び出しを使用した場合と同じ IL が生成されるため、拡張メソッドを使用してもパフォーマンスに影響はありません。

とはいえ、拡張メソッドはオブジェクト指向の概念に代わるものではありません。オブジェクト指向の概念に精通した、適切に設計されたアプリケーションを持つ開発者であれば、そのアプリケーションの中で既に継承を使用しているでしょうし、今後もそうするべきです。使用している型の多くを自分で所有しているのであれば、そのソース コードにアクセスし、通常のオブジェクト指向の概念で機能を拡張できます。拡張メソッドは、以前は拡張できなかったクラスを強化するための手段を提供するものです。ほかに適切な方法がない場合にのみ使用するようにしてください。

セキュリティの問題

既存のメソッドの目的の動作を乗っ取ったり妨害したりするために拡張メソッドが利用されるのではないかという懸念の声がよく聞かれますが、Visual Basic では、可能な場合には常に拡張メソッドよりインスタンス メソッドを優先することによって、この問題に対処しています。

Visual Basic では、拡張メソッドを使用して、既存のインスタンス メソッドのオーバーロードを異なるシグネチャで作成することができますが、既存のインスタンス メソッドのオーバーライドは防止されます。インスタンス メソッドと同じシグネチャを持つ拡張メソッドが存在する場合は、コンパイラに組み込まれているシャドウの規則によってインスタンス メソッドが優先されます。したがって、既存の基本クラスのインスタンスの機能が拡張メソッドによってオーバーライドされる可能性はなくなります (図 3 を参照)。

Figure 3 拡張メソッドのシャドウ (Close Figure 3)
Imports System.Runtime.CompilerServices

Module Module1
    Sub Main()
        Dim x As New ClsFoo
        Dim s As String = x.Method01
    End Sub
End Module

Class ClsFoo
    Function Method01() As String
        Return "Instance"
    End Function
End Class

Module ExtMethods
    <Extension()> _
    Function Method01(ByVal a As clsFoo) As String
        Return "Extension"
    End Function
End Module

拡張メソッドの実装では元の型は変更されないため、その追加機能は、その拡張メソッドを使用するように作成されたアプリケーションにしか認識されません。元の型を使用する既存のアプリケーションには違いはわかりません。

コンパイラによって生成される、基になる IL を見てみると、拡張メソッドの呼び出しが単純な共有メソッドの呼び出しに置き換えられていることがわかります。したがって、拡張メソッドを使用しても、標準の共有メソッドの呼び出しで行えること以外は何も行われないため、セキュリティ リスクが増大することはありません。

拡張メソッドによるオーバーロード

拡張メソッドを使用すると、インスタンス メソッドをオーバーロードすることができます。アプリケーションが正しく動作するようにするために、数多くの規則がコンパイラに組み込まれて、一貫したバインド動作が提供されるようになっています。そこでの目標は、拡張メソッドがインスタンス メソッドのように見えるだけでなく、一貫したエクスペリエンスも提供されるようにすることです。つまり、拡張メソッドを使用しているプロジェクトにレガシ コードを入力した場合、そのコードが従来と同じように動作する必要があります。

拡大変換を使用する、許容されるシグネチャを持つインスタンス メソッドは、ほとんどの場合、厳密に一致するシグネチャを持つ拡張メソッドより優先されます。その結果、拡張メソッドを使用するつもりなのにインスタンス メソッドにバインドされてしまう場合は、共有メソッドの呼び出し規約を使用して拡張メソッドを明示的に呼び出すことができます。2 つのメソッドのどちらとも明確にならない場合も、この方法であいまいさを取り除くことができます。

フィールドとプロパティは常に拡張メソッドを名前でシャドウします。図 4 は、同じ名前を持つ拡張メソッドとパブリック フィールドと、さまざまな呼び出しを示しています。拡張メソッドには 2 つ目の引数が含まれていますが、フィールドは拡張メソッドを名前でシャドウするため、この名前を使用した呼び出しはすべてフィールドへのアクセスになります。さまざまなオーバーロードされた呼び出しはすべてコンパイルされますが、実行時の結果は予測できません。これらはプロパティにバインドされ、プロパティの既定の動作を使用して 1 つの文字を返すか、ランタイム例外になるためです。拡張メソッドの名前を選択する際には、プロパティ、フィールド、および既存のインスタンス メソッドと競合しない名前を選択することが重要です。

Figure 4 同じ名前の拡張メソッドとパブリック フィールド (Close Figure 4)
Imports System.Runtime.CompilerServices

Module Module1
    Sub Main()
        Dim x As New Foo

        'Field - (Test)
        Console.WriteLine(x.TypeField)
        'Default Property Transformation on field resulting in T
        Console.WriteLine(x.TypeField(0))

        'Invalid Cast Exception Converting "fred" to integer
        'to use in default property transformation
        Console.WriteLine(x.TypeField("fred"))
    End Sub
End Module

Class Foo
    Public TypeField As String = "Test"
End Class

Module Extension
    <Extension()> _
    Function TypeField(ByVal a As Foo, ByVal x As String) As String
        Return "Extension"
    End Function
End Module

呼び出しのコードがどのメソッドにバインドされるかに関するコンパイラの規則は複雑です。これらの規則は、可能な場合には常に既存のインスタンス メソッドを選択する方針になっていますが、いくつかの例外もあります。一般的な考慮事項を次に示します。

  1. インスタンス メソッドは、ほとんどの場合に拡張メソッドより先に検討されます。拡大変換を使用するインスタンス メソッドは、拡張メソッドより先に検討されます。適用できないインスタンス メソッド (スコープ外のメソッドなど) は、オーバーロードの解決では検討されません。
  2. 型が正確に一致するシグネチャを持つインスタンス メソッドは、拡大変換を使用するメソッドより先に検討されます。
  3. クラスのプロパティやフィールドは、拡張メソッドを名前でシャドウします。

コンパイラによって選択されるメソッド (拡張メソッドかインスタンス メソッドか) が、拡張メソッドの名前とシグネチャによって大きく左右されることをよく理解しておく必要があります。

型の選択

もう 1 つの考慮事項は、拡張する型に関連することです。ほとんどすべての型を拡張できますが、だからといって拡張すればよいというものではありません。"暗黙的な再バインド" に注意する必要があります。これは、バインド先のメソッドが変更されたことが開発者にすぐにわからない状況を表します。エラーや警告は生成されず、別のメソッドが使用されることによって動作が変更されて初めて問題が発生したことがわかります。

たとえば、サードパーティ製のクラス ライブラリのメソッドをオーバーロードする拡張メソッドを作成し、アプリケーションが期待どおりに動作したとします。その後、そのサードパーティ製のツールの更新が提供され、それをコンピュータにインストールします。アプリケーションは問題なくコンパイルされ、エラーも生成されません。しかし、アプリケーションを実行すると正しく動作しません。この問題は、拡張したメソッドの新たなオーバーロードが更新されたコンポーネントに追加されていた場合に、結果として、拡張メソッドではなく、その追加されたインスタンス メソッドのオーバーロードが呼び出されるために発生します。

拡張メソッドは、バージョン管理のシナリオにおいて脆弱です。拡張メソッドの名前を使用する際に注意が必要なのはこのためでもあります。新しいオーバーロードで頻繁に更新される型で使用する場合は特に注意する必要があります。Option Strict On を使用すると、暗黙的な変換が行われる可能性がある状況では警告が生成されるため、暗黙的な再バインドの防止に役立ちます。

Object 型の拡張

Object 型を拡張することができます。Object 型を拡張すると、Object 型から派生するすべての型でそのメソッドが表示されるようになります。しかし、型をオブジェクトとして作成し、その型で拡張メソッドを使用しようとしても、拡張メソッドは表示されません。これは、拡張メソッドでは遅延バインドがサポートされていないためです。

図 5 の最初の呼び出しが機能するのは、変数 s の型が Integer で、Integer は Object から派生しているからです。その結果、拡張メソッドが表示されます。一方、変数 T は Object 型で、遅延バインドされるため、コンパイル エラーが発生します。

Figure 5 継承のサポート (Close Figure 5)
Imports System.Runtime.CompilerServices

Module Module1
    Sub Main()
        Dim s As Integer = 1234
        Console.WriteLine(s.Foo) '<- This will work

        Dim T As Object
        T = 1234
        Console.WriteLine(T.Foo)  '<- This will fail
    End Sub
End Module

Module Extensions01
    <Extension()> _
    Function Foo(ByVal b As Object) As String
        Return "Bar01"
    End Function
End Module

このコードを修正するには、次のように変更します。

Dim T = 1234
Console.WriteLine(T.Foo)

このコードが機能するのは、型の推定機能があるからです。変数 T は、代入される値に基づいて Integer として推定されます。これは、Visual Studio 2005 の場合とは異なります。Visual Studio 2005 では、T を、ボックス化された Integer を含む Object 型にします。

.NET Framework 2.0 アプリケーションの拡張メソッド

既に述べたように、拡張メソッドでは、基になる IL コードでは共有メソッドの呼び出しが作成されるだけで、Visual Basic 2005 でできないことは何も行われません。変わった点は、コンパイラで特定のメソッドが識別されるようになり、メソッドを呼び出す構文の変更が許可されるようになったことです。これにより、使用されている型に新しい機能があるように見せかけて、拡張メソッドを既存のインスタンス メソッドと同じように使用することができます。

Visual Basic 2008 には、複数バージョン対応という新機能もあります。これにより、Visual Basic 2008 を使用して、Framework の特定のバージョンをターゲットとするコードを記述することができます。使用されるコンパイラは変わらないため、たとえばバージョン 2.0 の Framework をターゲットとするアプリケーションに対して拡張メソッドを使用することも可能です。そのためには、ちょっとした回避策が必要です。というのも、.NET Framework 2.0 をターゲットとするアプリケーションに必要な参照を追加しようとすると、IDE によって阻止されるためです。

3.5 のアプリケーションを作成し、ターゲット Framework をバージョン 2.0 に変更すると ([プロジェクトのプロパティ] | [Compiler] (コンパイラ) | [Advanced] (詳細) | [Target Framework] (ターゲット Framework))、見つからない参照が強調表示されます。組み込みの LINQ 機能のほとんどは、これらの参照に含まれています。これらの参照を 2.0 プロジェクトに追加しようとすると、別のバージョンのターゲット Framework が必要であることを通知するダイアログが表示されます (図 6 を参照)。

図 6 .NET Framework 2.0 に変更されたターゲットの参照
図 6 .NET Framework 2.0 に変更されたターゲットの参照(画像を縮小するには、ここをクリックします)
図 6 .NET Framework 2.0 に変更されたターゲットの参照
図 6 .NET Framework 2.0 に変更されたターゲットの参照 (画像を拡大するには、ここをクリックします)

拡張メソッドを 2.0 アプリケーション用に作成したり使用したりするには、先に独自の System.Runtime.CompilerServices.Extension 属性を作成する必要があります。まず、バージョン 2.0 をターゲットとする新しいプロジェクトを作成します。既定の参照には、拡張属性を含む System.Core は含まれません。[Applications Project Properties] (アプリケーション プロジェクト プロパティ) で、[Application] (アプリケーション) タブに移動し、[Root Namespace] (ルート名前空間) チェック ボックスをオフにして、間違いなく System.Runtime.CompilerServices 名前空間に独自の拡張属性を作成します。図 7 のコードでは、拡張属性を作成し、それを使用して拡張メソッドを宣言および使用しています。アプリケーションをコンパイルすると、拡張メソッドを呼び出すことができます。

Figure 7 ExtensionAttribute クラスの作成 (Close Figure 7)
Imports System.Runtime.CompilerServices

Module Module1
    Sub Main()
        Dim i_x As Integer = 1
        Dim i_s As String = "Test"

        Dim x = i_x.IntegerExtension
        MsgBox(x)

        Dim y = i_s.StringExtension
        MsgBox(y.ToString)
    End Sub
End Module

'CREATE A USER-DEFINED ATTRIBUTE CALLED EXTENSION IN THE CORRECT NS
Namespace System.Runtime.CompilerServices
    <AttributeUsage(AttributeTargets.Assembly Or AttributeTargets.Class Or AttributeTargets.Method, AllowMultiple:=False, Inherited:=False)> Class ExtensionAttribute
        Inherits Attribute
    End Class
End Namespace

'DEFINED EXTENSION METHODS WITH USER-DEFINED ATTRIBUTE ON THEM
Module ExtMethods
    <Extension()> _
Function StringExtension(ByVal a As String) As String
        Return "Success"
    End Function

    <Extension()> _
Function IntegerExtension(ByVal a As Integer) As Integer
        Return 100
    End Function
End Module

ここで、2.0 をターゲットとするアプリケーションに関する重要な考慮事項について述べておきます。まず、型の推定は既定で無効になっています。有効にするには、Option Infer On を使用します。.NET Framework 2.0 アプリケーションをバージョン 3.5 に移行する際には、おそらくこの作業が必要になります。

次に、拡張メソッドを使用するには、System.Core の拡張属性と同様の拡張属性を独自に作成する必要があります。2.0 をターゲットとするプロジェクトで System.Core の参照を使用しようとしても、IDE によって阻止されます。

最後に、この手法は、.NET Framework 2.0 をターゲットとする ASP.NET アプリケーションでは使用できません。.NET Framework 2.0 をターゲットとする ASP.NET アプリケーションには、2.0 のコマンド ライン コンパイラに対する実行時の依存関係があるからです。.NET Framework 2.0 しかインストールされていない Web サーバーにそれらのアプリケーションを配置しても、2.0 の VBC.EXE コマンド ライン コンパイラでは拡張メソッドは認識されないため、機能しません。

まだ .NET Framework 3.5 用アプリケーションの作成を始めない場合や、保守しなければならないバージョン 2.0 の既存アプリケーションがある場合でも、Visual Basic 2008 にアップグレードするメリットはあります。Visual Basic 2008 では、拡張メソッドの機能がコンパイラに組み込まれており、Visual Basic 2008 コンパイラでターゲットにすることのできるあらゆるバージョンの Framework に対してこの機能を使用できます。

ご意見やご質問は、

こちら instinct@microsoft.com.

まで英語でお送りください。
Adrian "Spotty" Bowles は、すべてのバージョンの Visual Basic による開発を経て、現在はワシントン州レドモンドで、Visual Basic Product チームのソフトウェア設計エンジニア/テスタとして、Visual Basic コンパイラを担当しています。Visual Basic 2008 リリースでは、拡張メソッドを含むさまざまな言語機能を担当しました。連絡先は Abowles@microsoft.com (英語) です。


Office Space: SharePoint 2007 のイベント

Office Space
SharePoint 2007 のイベント
Ted Pattison

コードのダウンロード : : OfficeSpace2007_11.exe (167KB)
コンテンツ
イベント レシーバ クラス
Before イベントと After イベント
Before イベントを検証する
After イベント ハンドラ
ハンドラをイベントにバインドする
WSS オブジェクト モデルを使用する

Windows® SharePoint® Services (WSS) 3.0 では、SharePoint サイトに基づくカスタム ビジネス ソリューションを構築する開発者のために、さまざまな点が大幅に改善されています。開発者向けの最大の強化点の 1 つは、サーバー側のイベントを処理するための新しいインフラストラクチャに関連しています。たとえば、ユーザーが SharePoint サイトでコンテンツを変更する操作を実行するときに (新しいドキュメントのアップロードやリストからのアイテムの削除など)、開発者は、C# または Visual Basic® で記述されたサーバー側ロジックを実行するイベント ハンドラを使用して、このユーザーの操作に応答することができます。

WSS 2.0 もサーバー側のイベント処理のインフラストラクチャを備えていましたが、WSS 3.0 ではさまざまな点が大幅に改善されています。図 1 に、WSS 2.0 と WSS 3.0 におけるイベント処理サポートの違いの概要を示します。WSS 3.0 の新しいイベント処理インフラストラクチャが、以前のバージョンに既に存在していた機能の単なる改良版ではないことを理解することが重要です。WSS 3.0 の新しいイベント処理インフラストラクチャは、WSS 2.0 のイベント処理インフラストラクチャを完全に代替します。最大の変更点の 1 つは、イベント処理がドキュメント ライブラリ レベルではなくリスト レベルで実装されることです。つまり、リスト定義やリスト アイテムで発生するイベントが、ドキュメント ライブラリを含むすべての種類のリストで動作します。

Figure 1 WSS 2.0 と WSS 3.0 のイベント処理 (Close Figure 1)

イベントの種類 | WSS 2.0 インフラストラクチャ | WSS 3.0 インフラストラクチャ
ドキュメント ライブラリのイベント | サポートされています | サポートされています
1 つのイベントにバインドされた複数のイベント ハンドラ | サポートされていません | サポートされています
リストのイベント | サポートされていません | サポートされています
リスト定義のイベント | サポートされていません | サポートされています
サイトのイベント | サポートされていません | サポートされています
キャンセルに対応できる事前の同期イベント ハンドラ | サポートされていません | サポートされています
事後の非同期イベント ハンドラ | サポートされています | サポートされています
イベント ハンドラ クラス | IListEventSink インターフェイスの実装が必要 | WSS オブジェクト モデルのレシーバ基本クラスの継承が必要

WSS 2.0 の古いインフラストラクチャでは、まったく異なるプログラミング モデルが使用され、イベント ハンドラをイベントに関連付けるために異なる方法が必要です。WSS 3.0 では、バージョン 3.0 の新しいイベント処理インフラストラクチャと WSS 2.0 の古いインフラストラクチャの両方をサポートします。ただし、WSS 3.0 での古いバージョン 2.0 形式のイベント ハンドラのサポートは、WSS 2.0 ソリューション用に記述されたコードを WSS 3.0 環境に統合する場合の下位互換性を提供するためだけに含まれていることを理解しておく必要があります。WSS 3.0 および Microsoft® Office SharePoint Server (MOSS) 2007 のソリューションの開発を開始する場合は、WSS 3.0 の新しいイベント処理インフラストラクチャを使用してください。このインフラストラクチャは、新しい概念と今月の Office Space コラムで説明するプログラミング モデルに基づいています。

イベント レシーバ クラス

WSS 3.0 のイベント処理は、イベント レシーバ クラスに基づきます。具体的には、WSS オブジェクト モデル内のパブリック クラスとして用意されているいくつかのイベント レシーバ基本クラスのいずれかを継承することで、新しいイベント レシーバ クラスを作成します。それには、Microsoft.SharePoint.dll という WSS のコア アセンブリを参照する新しいクラス ライブラリ プロジェクトを作成する必要があります。

WSS オブジェクト モデルを通じて利用できる WSS レシーバ基本クラスの一覧を図 2 に示します。WSS レシーバ基本クラスのいずれかを継承するクラス定義を作成したら、選択したメソッドをオーバーライドしてイベント ハンドラを追加します。

Figure 2 WSS イベント レシーバ基本クラス (Close Figure 2)

クラス | 説明
SPListEventReceiver | ユーザーがリスト定義の列を追加、変更、および削除するときのイベント処理を提供します。
SPItemEventReceiver | ユーザーがリスト内のアイテムを変更するとき、またはドキュメント ライブラリ内のドキュメントを変更するときのイベント処理を提供します。
SPWebEventReceiver | ユーザーがサイト コレクションまたはサイトを移動または削除するときのイベント処理を提供します。
SPEmailEventReceiver | ユーザーが電子メール対応のリストに電子メールを送信するときのイベント処理を提供します。

1 つ以上のイベント ハンドラを持つカスタム レシーバ クラスを実装したら、厳密な名前を指定してアセンブリにコンパイルし、グローバル アセンブリ キャッシュ (GAC) にインストールする必要があります。次に、サイト、リスト、またはドキュメント ライブラリなどのホスト オブジェクトで公開されるイベントにイベント ハンドラをバインドできます。このコラムの後半で説明するように、イベント ハンドラのバインドについては 2 つの選択肢があります。イベント ハンドラは、宣言型 XML 構文を含む機能を使用してバインドすることも、WSS オブジェクト モデルを使用してプログラムでバインドすることもできます。イベント ハンドラ メソッドを特定のイベントにバインドすれば、後は WSS によって、イベントをトリガするユーザーの操作に応じてハンドラの実行が処理されます。

Before イベントと After イベント

WSS 3.0 イベントは、Before イベントと After イベントの 2 つの主なカテゴリに分類されます。Before イベントは、ユーザーの操作に応じて、WSS がコンテンツ データベースにデータを書き込む直前に発生します。重要な点は、Before イベントは要求処理ライフサイクルの早い段階で発生するので、ユーザーの操作のキャンセルがサポートされることです。したがって、Before イベントでは、セキュリティ チェックやカスタム検証を実行する重要な機会が提供されます。

WSS は After イベントもサポートします。After イベントのイベント ハンドラは、ユーザーの操作をコミットするため WSS 3.0 がコンテンツ データベースに書き込んだ後に実行されます。After イベントでは、ユーザーの操作のキャンセルはサポートされません。むしろ、After イベントは、コンテンツが変更されるたびに実行するように設計されたカスタム処理ロジックを開始するために使用します。

Before イベントのイベント ハンドラは、ユーザーの操作 (アイテムの追加など) を処理している同じスレッドで実行されます。そのため、Before イベントにはブロックを行う性質があり、一般的に同期イベントと呼ばれます。WSS では After イベントのイベント ハンドラを個別のスレッドで実行するので、ユーザーに送信する応答をその処理がブロックすることはありません。この理由から、After イベントは一般的に非同期イベントと呼ばれます。Before イベントは "ing" で終わる名前 (たとえば、FieldDeleting) を持つオーバーライド可能なメソッドに基づき、一方の After イベント メソッドの名前は "ed" で終わる (たとえば、FieldDeleted) ことにも注意してください。

Before イベントおよび After イベントのイベント ハンドラを定義する新しいカスタム レシーバ クラスを作成する例を見てみましょう。図 3 に、SPListEventReceiver クラスを継承し、FieldDeleting メソッドと FieldDeleted メソッドをオーバーライドするカスタム レシーバ クラスを示します。WSS でこれらのイベント ハンドラのいずれかを呼び出すとき、properties という名前の SPListEventProperties 型の 1 つのパラメータを渡すことに注意してください。

Figure 3 カスタム レシーバ クラス (Close Figure 3)
Imports Microsoft.SharePoint

Public Class MyListEvents
    Inherits SPListEventReceiver

    Public Overrides Sub FieldDeleting ( _
        ByVal properties As SPListEventProperties)
    '*** your custom event handling code goes here
    End Sub

    Public Overrides Sub FieldDeleted ( _
        ByVal properties As SPListEventProperties)
    '*** your custom event handling code goes here
    End Sub

End Class

FieldDeleting メソッドは Before イベントを処理するので、ユーザーの操作のキャンセルをサポートします。サイト コレクション管理者を含むすべてのユーザーに対して、特定のリスト定義から列を削除することを禁止するシナリオを考えてみましょう。これを実現するには、properties という名前の SPListEventProperties パラメータの Cancel プロパティに True の値を代入します。ユーザーの操作をキャンセルする場合は、ErrorMessage プロパティに文字列値を代入してカスタム エラー メッセージを割り当てる必要もあります。このエラー メッセージは、ユーザーに直接表示されます。

Public Overrides Sub FieldDeleting( _
    ByVal properties As SPListEventProperties)
    '*** cancel user action
    properties.ErrorMessage = "Deleting not supported!"
    properties.Cancel = True
End Sub

Before イベントのイベント ハンドラで Cancel プロパティに True の値を代入すると、WSS は応答として、要求を省略してユーザーの操作をキャンセルします。ユーザーから要求された列の削除の代わりに、WSS はカスタム エラー メッセージをユーザーに表示します。ここでの重要な点は、Before イベントによって望ましくない変更に対する防御層を構成できることです。

Before イベントのイベント ハンドラでユーザーの操作をキャンセルする基本について説明したので、条件付きロジックを使用して Before イベントのイベント ハンドラをもう 1 つ作成しましょう。要件として、リストからアイテムを削除したり、ドキュメント ライブラリからドキュメントを削除したりすることをユーザーに許可する前に、そのユーザーが特定の条件を満たしていることを確認するセキュリティ チェックを実行する必要があるとしましょう。図 4 に示すレシーバ クラス定義の ItemDeleting イベント ハンドラを見てください。条件付きロジックによって、サイト管理者のアクセス許可をユーザーが所有していないと判断した場合は、そのユーザーの操作をキャンセルすることがわかります。

Figure 4 簡単なセキュリティ チェック (Close Figure 4)
Public Class MyItemEvents
    Inherits SPItemEventReceiver

    Public Overrides Sub ItemDeleting( _
        ByVal properties As SPItemEventProperties)

        If (Not properties.OpenWeb().CurrentUser.IsSiteAdmin()) Then
            Dim ErrorMessage As String
            ErrorMessage = "Only site administrator can delete items"
            properties.ErrorMessage = ErrorMessage
            properties.Cancel = True
        End If

    End Sub

End Class

ご覧のとおり、このイベント ハンドラのロジックでは、削除の発生を許可する前に現在のユーザーが特定のアクセス許可を持っていることを要求します。この例を一歩進めると、ユーザーが Active Directory® グループのメンバであるかどうかによって、そのユーザーがアイテムを削除できるかどうかを判断するイベント ハンドラを記述できます。WSS は ASP.NET ランタイムの上に構築されるので、簡単な ASP.NET プログラミング手法でこれを実現し、HttpContext オブジェクトの User プロパティによって公開される IsInRole メソッドに対してクエリを実行できます。

If (Not HttpContext.Current.User.IsInRole( _
    "MyDomain\ContentManagers")) Then
    Dim ErrorMessage As String
    ErrorMessage = _
    "Only members of ContentManagers can delete items"
    properties.ErrorMessage = ErrorMessage
    properties.Cancel = True
End If

Before イベントを検証する

次は、Before イベントのイベント ハンドラで検証ロジックを記述する場合の可能性を検討しましょう。これによって、ユーザーがリスト内のアイテムを追加したり、変更したりする場合に、アプリケーション固有の制約にユーザーが従うようにするイベント ハンドラを記述できます。たとえば、納品書の出荷日が土曜日または日曜日でないかどうかや、電話番号や電子メール アドレスが正規表現で定義された形式に沿っているかどうかを検証できます。

Before イベントの検証ロジックの記述における最も強力な側面の 1 つは、複数の列にまたがる検証を実行できることです。今月のサンプル コードでは、勤務先電話番号の列と電子メール アドレスの列を含む Sales Leads (潜在顧客) リストを使用します。作成するイベント ハンドラは、勤務先電話番号または電子メール アドレスのいずれかに空白以外の値が含まれていることを要求する Sales Leads リストを使用して、各アイテムに対して検証を実行します。ここでの基本的な考え方は、連絡先情報が何も入力されていない場合は、潜在顧客が有効でないということです。

ユーザーがリスト アイテムを変更する方法は 2 つあります。ユーザーは、新しいアイテムを追加するか、既存のアイテムを更新できます。Before イベントの検証ロジックを記述する場合、通常は、ItemAdding イベントと ItemUpdating イベントにバインドされたイベント ハンドラの冗長コードを追加する必要があります。したがって、2 つの異なるイベント ハンドラから呼び出すことができる個別のメソッドで検証ロジックを保持するのが最善です。図 5 に示す SalesLeadIsValid メソッドは、Phone および E-mail という名前の 2 つの文字列パラメータを受け取り、これらのパラメータのいずれかが空白でない場合に True を返します。

Figure 5 入力の検証 (Close Figure 5)
Private Function SalesLeadIsValid(
    ByVal Phone As String, ByVal Email As String) _
    As Boolean

    '*** make sure we have either phone or e-mail
    If (String.IsNullOrEmpty(Phone) And _
        String.IsNullOrEmpty(Email)) Then
        Return False
    Else
        Return True
    End If

End Function

SalesLeadIsValid メソッド内の検証ロジックでは、ItemAdding イベントと ItemUpdating イベントにバインドされたイベント ハンドラからこのメソッドを呼び出すことができます。ItemAdding イベントまたは ItemUpdating イベントのイベント ハンドラ内で、検証を実行する対象の列の値を取得する必要があることに注意してください。SPItemEventProperties パラメータは、BeforeProperties および AfterProperties という 2 つのコレクションを公開します。つまり、ユーザーが列の値を変更する前または変更した後の状態のどの列の値も取得できます。

Phone と E-mail の両方の After 値を取得し、ユーザーの操作を成功させるかどうかを判断する SalesLeadIsValid メソッドにそれらの値を渡す ItemAdding イベント ハンドラの実装例を図 6 に示します。

Figure 6 操作の検証 (Close Figure 6)
Public Overrides Sub ItemAdding( _
    ByVal properties As SPItemEventProperties)

    Dim Phone As String, Email As String
    Phone = _
        CType(properties.AfterProperties("WorkPhone"), _
        String)
    Email = _
        CType(properties.AfterProperties("EMail"), _
        String)

    If (Not SalesLeadIsValid(Phone, Email)) Then
        Dim ErrorMessage As String
        ErrorMessage = _
            "Sales lead must have valid phone or email"
        properties.ErrorMessage = ErrorMessage
        properties.Cancel = True
    End If
End Sub

このイベント ハンドラで AfterProperties コレクションから 2 つの列の値を取得する方法を詳しく見ていきましょう。AfterProperties は、列の名前を受け取る既定のパラメータ化されたプロパティ (インデクサ) を持つコレクションです。AfterProperties に渡すパラメータは、大文字と小文字が区別される文字列値であることに注意してください。また、このパラメータが表示名ではなく列の物理名を表すことを理解しておく必要があります。

After イベント ハンドラ

After イベントは、操作が完了した後に発生し、操作をキャンセルする機会を開発者に提供しないため、Before イベントとは異なります。After イベントは、SharePoint サイト内のコンテンツを変更した操作をユーザーが正常に完了したときに実行する必要があるカスタム処理を開始する機会を開発者に提供します。たとえば、ユーザーが新しいアイテムをリストに追加したら、After イベントを使用して、いずれかの列の書式を再設定してデータの整合性を維持したり、通知電子メール メッセージを特定のユーザーや配布リストに送信したりできます。

ItemAdded イベントにバインドされる Sales Leads リストのイベント ハンドラを記述する例を見てみましょう。この例では、すべての潜在顧客の名前をすべて大文字に設定する要件があります。イベント ハンドラは、潜在顧客の名前を含む列の値を取得し、大文字を使用してその値の書式を再設定します。

After イベントのイベント ハンドラを記述する場合に、イベントが基づくアイテムを変更するときは、慎重に行う必要があります。問題は、アイテムを変更するとイベントが再び発生することです。イベントが再び発生するとアイテムを変更するので、再帰ループが開始します。これを回避するには、アイテムの変更中は現在のアイテムでのイベントの発生を無効にする必要があります。これは、基になる基本クラスで提供される DisableEventFiring メソッドを呼び出すことで行います。アイテムを変更したら、次は EnableEventFiring を呼び出す必要があります。すべてのパーツを組み合わせると次のようになります。

Public Overrides Sub ItemUpdated( _
    ByVal properties As SPItemEventProperties)

    Me.DisableEventFiring()
    Dim SalesLead As String = _
        CType(properties.ListItem("Title"), String)
    '*** reformat item column values as required
    properties.ListItem("Title") = SalesLead.ToUpper()
    properties.ListItem.Update()
    Me.EnableEventFiring()
End Sub

Before イベントと After イベントのイベント ハンドラを記述するいくつかの異なる例を見てきました。イベント ハンドラを記述する理由と方法について理解が深まったはずです。残された主な疑問は、SharePoint サイト内のホスト オブジェクトのイベントにイベント ハンドラをバインドする方法です。次に、それを見ていきましょう。

ハンドラをイベントにバインドする

既に説明しましたが、レシーバ クラスは厳密な名前を指定してアセンブリ DLL にコンパイルし、GAC にインストールする必要があります。イベント ハンドラをバインドする場合は、機能で宣言型 XML を使用するか、WSS オブジェクト モデルを使用してプログラムで 2 つをバインドするかを選択できます。ただし、どちらの方法でも、名前空間修飾名に加えて、バインドするイベント ハンドラを含むレシーバ クラスの 4 つの部分から成るアセンブリの完全名を知っている必要があります。

まず、機能内で宣言型 XML を使用してイベント ハンドラを特定のリストの種類にバインドする方法について説明します。これは、1 つ以上の内部 Receiver 要素を含む Receivers 要素を追加することで実現できます。このコラムに付属するサンプル コードでは、2 つの異なるイベント ハンドラを WSS の連絡先リストにバインドする Receivers 要素を含む機能があります (図 7 参照)。連絡先リストのリストの種類識別子は 105 です。これは、この機能をアクティブ化したサイト内の連絡先リストから作成されるすべてのリストが、自動的にこれらのイベント ハンドラにバインドされることを示します。

Figure 7 連絡先リスト レシーバ (Close Figure 7)
<Elements xmlns="http://schemas.microsoft.com/sharepoint/">

  <!-- Receivers element used only in feature where Scope=Web -->
  <Receivers ListTemplateId="105" >

    <Receiver>
      <Name>Field_Deleting_Event</Name>
      <Type>FieldDeleting</Type>
      <Assembly>OfficeSpaceEventDemo, [4-part assembly name]</Assembly>
      <Class>OfficeSpaceEventDemo.MyListEvents</Class>
      <SequenceNumber>1000</SequenceNumber>
      <Data>My Custom Data</Data>
    </Receiver>

    <Receiver>
      <Name>Field_Deleted_Event</Name>
      <Type>FieldDeleted</Type>
      <Assembly>OfficeSpaceEventDemo, [4-part assembly name]</Assembly>
      <Class>OfficeSpaceEventDemo.MyListEvents</Class>
      <SequenceNumber>1000</SequenceNumber>
      <Data>My Custom Data</Data>
    </Receiver>

  </Receivers>
</Elements>

Receivers 要素内で、バインドするイベント ハンドラごとに個別の Receiver 要素を指定する必要があることに注意してください。1 つの Receiver 要素で複数のイベント ハンドラを一度にバインドする手法はサポートされていません。

Receiver 要素には、2 つの必須のバインド情報があります。それは Assembly 要素と Class 要素で、それぞれ、4 つの部分から成るアセンブリの完全名と名前空間修飾レシーバ クラス名を格納する必要があります。Receiver 要素内の Name 要素は、フレンドリ名 (わかりやすい名前) を追跡目的でイベント ハンドラに割り当てるために使用できますが、必須ではありません。

Receiver 要素内の SequenceNumber 要素は、1 つのイベントにバインドされた複数のイベント ハンドラを使用する場合に役立ちます。連番のより小さいイベント ハンドラは、連番のより大きいイベント ハンドラよりも常に先に発生します。したがって、イベント ハンドラを複数単位で追加して、一定の実行順序が保証されるようにすることができます。

Receiver 要素内の Data 要素では、任意のカスタム文字列データを使用して特定のイベント ハンドラのバインドを構成できます。SPListEventProperties パラメータの ReceiverData プロパティを使用して Data 要素内からこの文字列値にアクセスし、その値をアプリケーション固有の方法で使用して、検証ルーチンやカスタム エラー メッセージに対して数値や正規表現を提供できます。

Public Overrides Sub FieldDeleting( _
    ByVal properties As SPListEventProperties)

    Dim BindingData As String
    BindingData = properties.ReceiverData
  '*** use binding data in application-specific fashion
End Sub

WSS オブジェクト モデルを使用する

機能で Receivers 要素を使用して宣言によってイベント ハンドラをイベントにバインドすることは、一部のシナリオでは機能しますが、他の多くのシナリオで必要とされる柔軟性に欠けます。たとえば、Receivers 要素を使用できるのは、1 つのサイト全体を単位とする特定のリストの種類にイベント ハンドラをバインドするためだけです。この宣言型の方法では、イベント ハンドラをサイトの 1 つのリスト インスタンスだけにバインドする手段はなく、同じ種類のその他のすべてのリストにもバインドされてしまいます。

イベント ハンドラを利用するカスタム SharePoint ソリューションの設計時、コードでイベント ハンドラをバインドすると、はるかに高い柔軟性を得られることがわかります。たとえば、1 つのリストだけのイベントにイベント ハンドラをバインドできます。

ItemAdding イベントのイベント ハンドラを Sales Leads リストにバインドする例を見てみましょう。

Dim SalesLeadsList As SPList = Me.Web.Lists("Sales Leads")
    '*** bind event handler to ItemAdding event
    SalesLeadsList.EventReceivers.Add( _
        SPEventReceiverType.ItemAdding, _
        GetType(MyItemEvents).Assembly.FullName, _
        GetType(MyItemEvents).FullName)

各 SPList オブジェクトは、EventReceivers コレクションを公開します。EventReceivers コレクションは、3 つの異なるオーバーロードを実装した Add メソッドを公開します。この最後の例の Add メソッドは最も簡単な方法を示しており、3 つのパラメータを渡す必要があります。コードによってイベント ハンドラをバインドする場合は、最小限、アセンブリの名前、名前空間修飾クラス名を指定し、WSS オブジェクト モデルの SPEventReceiverType 列挙体を使用してイベントの種類を指定する必要があります。

4 つの部分から成るアセンブリ名と名前空間修飾レシーバ クラス名を含むリテラル文字列値をコード内に埋め込むことができますが、前に示したコードを使用してこれらの値を動的に検出することもできます。

Add メソッドの呼び出しが最も簡単な方法を示していますが、すべてのシナリオには対応できません。図 8 は、イベント ハンドラのバインドをプログラムによって SPEventReceiverDefinition オブジェクトに対して直接実行するより複雑な方法を示しています。これによって特定の GUID 識別子をバインドに使用できるので、それらを簡単に追加および削除できるようになります。また、SequenceNumber プロパティや Data プロパティを初期化できるようにもなります。

Figure 8 GUID に対するバインド (Close Figure 8)
Dim SalesLeadsList As SPList = Me.Web.Lists("Sales Leads")

Dim receivers As SPEventReceiverDefinitionCollection
receivers = SalesLeadsList.EventReceivers
Dim ItemAddingDefId As Guid
ItemAddingDefId = New Guid("D6C452E5-95D4-45e8-934C-30F785867000")
If (Not receivers.EventReceiverDefinitionExist(ItemAddingDefId)) Then
    Dim ItemAddingDef As SPEventReceiverDefinition
    ItemAddingDef = receivers.Add(ItemAddingDefId)
    ItemAddingDef.Name = "ItemAddingHandler"
    ItemAddingDef.Type = SPEventReceiverType.ItemAdding
    ItemAddingDef.Assembly = GetType(MyItemEvents).Assembly.FullName
    ItemAddingDef.Class = GetType(MyItemEvents).FullName
    ItemAddingDef.Data = "My Custom Data"
    ItemAddingDef.SequenceNumber = 1001
    ItemAddingDef.Update()
End If

WSS オブジェクト モデルを使用してイベント ハンドラをバインドする場合の可能性についてごく簡単に説明してきました。ここではリストの EventReceivers コレクションの操作を示しましたが、WSS オブジェクト モデルのその他のさまざまな種類に EventReceivers コレクションがあり、イベント ホストとして動作できることを知っておく必要があります。そのような種類の例には、サイト、コンテンツ タイプ、ファイルなどがあります。

ご意見やご質問は、

Ted mmoffice@microsoft.com.

まで英語でお送りください。
Ted Pattison は、フロリダ州タンパ在住で、執筆、教育活動に従事しています。また、SharePoint MVP でもあります。Ted は Microsoft Press 発行の『Inside Windows SharePoint Services 3.0』の著者です。彼の会社 Ted Pattison Group (www.TedPattison.net) では、企業の開発者を対象とする SharePoint の上級者向けトレーニングを実施しています。


Cutting Edge: AJAX におけるユーザー エクスペリエンスを管理する

Cutting Edge
AJAX におけるユーザー エクスペリエンスを管理する
Dino Esposito

コードのダウンロード : : CuttingEdge2007_11.exe (183KB)
コンテンツ
AJAX とユーザー インターフェイス
要素 ID を使用して UI を無効にする
名前付けコンテナ内の UI 要素を無効にする
UpdatePanelAnimation エクステンダを使用する
ページ全体を無効にする
一度に 1 つの AJAX ポストバック
優先度の高い呼び出し

Web の "ストップ アンド ゴー" 的な性質に私たちは慣れています。ユーザーがボタンやリンクをクリックして要求を行うたびに、ブラウザが要求を行い応答を待機します。ユーザーはそのページで少し操作した後、再度、クリックして待機します。

最近のいくつかのコラムでも書いたとおり、私が非常に期待しているのは、AJAX パラダイムとその関連テクノロジが提供する可能性です。開発者やアーキテクトは、ストップ アンド ゴーのパターンの代わりに対話的な構造を持つ、新世代の Web サイトを構築できるのです。AJAX アプリケーションの連続感を初めて味わうユーザーがすっかり熱中するのを見ると、うれしくなります。

最近のコラムで私は、直接間接の利点を多数挙げて AJAX の威力を賞賛してきました。今月は、ASP.NET AJAX Extensions を使用した AJAX パラダイムの実装に直接関係する、いくつかの問題について解説します。

当初 ASP.NET 2.0 の上に構築されていた ASP.NET AJAX Extensions は、ASP.NET 3.5 のリリースと共に、ASP.NET プラットフォームに完全に統合されました。実際 ASP.NET AJAX では、部分的なレンダリングおよびスクリプト サービスの 2 種類のプログラミング モデルから選択可能です。これら 2 種類の詳細については、MSDN® Magazine 2007 年 9 月号と 2007 年 10 月号の私のコラムを参照してください。

AJAX アプリケーションには、ブラウザを通さずにスクリプト コードから直接、Web 要求を行う機能があります。ブラウザを使用して (標準的な送信ボタンやハイパーリンクなどで) ページを要求すると、アクティブなブラウザ ウィンドウごとに一度に 1 つの要求だけが処理されます。スクリプトによるページ要求にはこの制約がなく、複数の要求を同時に行うことができます。2007 年 7 月号の Cutting Edge コラムでは、この機能を利用し、サーバー側で実行されているタスクの実際の進行状況をクライアントに通知する、ライブ進行状況バーを実装しました。1 つのブラウザ ウィンドウから複数の呼び出しを同時に行うことができれば、UI 関連の多くの問題など、さまざまな問題に対処できる可能性が格段に広がります。

今月のコラムでは、ASP.NET AJAX ベースの Web アプリケーションに効果的なユーザー インターフェイスを実装する際の問題、解決策、および必要なツールについて説明します。特に、部分的なレンダリング モデルを使用したアプリケーションに焦点を絞って説明します。

AJAX とユーザー インターフェイス

ユーザーが AJAX 要求を行った後も、Web ページはアクティブなままです。このため、ユーザーはアクティブな要素をクリックして新しい操作を開始できますが、それによって現在の要求が干渉を受ける可能性があります。よって、デスクトップ アプリケーションにおいては、UI の一部を一時的に無効にするよう、開発者が考慮しなければならない場合もあります。

サーバー操作の間、クライアント ページの要素を無効にするには、どうすればよいでしょうか。これにはいくつかの方法があります。対応するドキュメント オブジェクト モデル (DOM) 要素の ID を取得し、その状態を有効または無効にできます。このクライアント側操作は、DOM (または場合によって CSS スタイル) を使用してスクリプトで行います。また、ASP.NET コントロールには Visible プロパティがあります。Visible プロパティを false に設定すると、そのサーバー コントロールのマークアップがクライアント応答に追加されなくなります。その結果、そのコントロール用のマークアップはクライアント ページに表示されません。

ASP.NET AJAX で一部のユーザー インターフェイスを無効または有効にするには、まず、その部分を構成する HTML 要素すべての ID を特定する必要があります。次にコードを示します。

// Get the reference to the DOM element
var button1 = $get("Button1");

// Disable the status of the element
button1.disabled = true;

要素を無効にするには、ID を取得し、disabled プロパティを適切な値に設定します。ユーザー インターフェイスの特定部分を、要求を行う直前に無効にし、要求が完了した直後に再度有効にするのが理想的です。選択したプログラミング モデルによって、実装は異なります。

部分的なレンダリングを使用している場合、PageRequestManager オブジェクトのイベント モデルを使用します。このクラスは、部分的なレンダリングの背後にあるクライアント側の頭脳です。完全なクラス名は Sys.WebForms.PageRequestManager で、そのソース コードは MicrosoftAjaxWebForms.js に定義されています。PageRequestManager には、AJAX ポストバックの設定と制御に必要なロジックがすべて含まれています。このクラスはシングルトンです。つまり、一度に保留される要求は必ず 1 つです。ページ要求マネージャの一意のインスタンスへの参照を取得するコードは、次のとおりです。

var manager = Sys.WebForms.PageRequestManager.getInstance();

図 1 に示すように、PageRequestManager によって公開されているいくつかのクライアント側イベントを使用すると、AJAX ポストバックの処理における主要なステップをインターセプトできます。通常、BeginRequest イベントの発生時に UI 要素を無効にし、EndRequest イベントの発生時に HTML 要素を再度有効にします。

Figure 1 PageRequestManager によって公開されているイベント (Close Figure 1)

イベント | 引数 | 説明
initializeRequest | InitializeRequestEventArgs | 新しい AJAX 要求の発行を検出した直後に発生します。
beginRequest | BeginRequestEventArgs | 要求の送信直前に発生します。
pageLoading | PageLoadingEventArgs | 応答が完全にダウンロードされて処理された後、ページの内容が更新される前に発生します。
pageLoaded | PageLoadedEventArgs | ページのすべての内容が更新された後に発生します。
endRequest | EndRequestEventArgs | すべての作業の完了時に発生します。

スクリプト サービス アプローチを選択した場合は、ASP.NET AJAX スクリプト Web サービスで構成されたサーバー側ファサードと通信する、JavaScript で稼動するフロントエンドが存在する可能性があります。サービスに対する各呼び出しは、明示的な非同期スクリプト呼び出し経由で行われます。開発者は、リモート呼び出しの開始を完全に制御できるため、UI の任意の部分を安全に無効にできます。各呼び出しが非同期であるため、呼び出しの成功と失敗の両方に対してコールバックを指定可能です。よって、コールバックから、UI を適切な状態にリセットできます。

ただし、UI 要素の無効化と再有効化には問題もあります。いくつかのシナリオに沿って説明していきます。

要素 ID を使用して UI を無効にする

図 2 は、サンプルの AJAX ページ (部分的なレンダリングを使用) でポストバック中に一部の要素を無効にするためのコードです。ページの初回読み込み時に、BeginRequest イベントおよび EndRequest イベントのハンドラが登録されます。pageLoad 関数は、AJAX クライアント ライブラリによって起動されるアプリケーション読み込みイベントのハンドラであり、慣例に従って名付けられています。pageLoad 関数は 2 つの引数を受け取ります。1 つは Sys.Application 型、もう 1 つは ApplicationLoadEventArgs オブジェクトです。

Figure 2 ポストバック中に UI を無効にするページ (Close Figure 2)
<html>
<body>

<script type="text/javascript">
function pageLoad()
{
    var manager = Sys.WebForms.PageRequestManager.getInstance();
    manager.add_beginRequest(OnBeginRequest);
    manager.add_endRequest(OnEndRequest);
}
var lcPostbackElementID;
functionw OnBeginRequest(sender, args)
{
    lcPostbackElementID = args.get_postBackElement().id.toLowerCase();
    if (lcPostbackElementID === "button1")
    {
        $get("Button1").disabled = true;
    }
}

function OnEndRequest(sender, args)
{
    if (lcPostbackElementID === "button1")
    {
        $get("Button1").disabled = false;
    }
}
</script>

<form id="form1" runat="server">

    <asp:ScriptManager ID="ScriptManager1" runat="server" />

    <div id="pageContent">

        <b>Enter some data:</b><br />
        <asp:TextBox ID="TextBox1" runat="server" />
        <asp:Button ID="Button1" runat="server" Text="Post ..."
            onclick="Button1_Click" />
        <br /><br />

        <asp:UpdateProgress runat="server" ID="UpdateProgress1">
            <ProgressTemplate>
                <img src="images/loading.gif" />
            </ProgressTemplate>
        </asp:UpdateProgress>

        <asp:UpdatePanel ID="UpdatePanel2" runat="server"
            UpdateMode="Conditional">
            <ContentTemplate>
                <asp:Label runat="server" ID="Label1" />
            </ContentTemplate>
            <Triggers>
                <asp:AsyncPostBackTrigger ControlID="Button1" />
            </Triggers>
        </asp:UpdatePanel>
    </div>

</form>
</body>
</html>

BeginRequest (および図 1 に示したその他のイベントすべて) は、ページ内から行われる AJAX ポストバックごとに発生します。これらのイベントは、要求が実行中であることや完了したことを開発者に通知するだけです。これらのイベントは、クライアント ボタンやサーバー ボタンのクリック イベントなどとは異なります。ポストバックが行われることはわかりますが、その理由や、どの UI 要素に関係するのかは、すぐにはわかりません。しかし、ユーザー インターフェイスの適切な部分を無効にするには、どの要素がクリックされたのかを正確に知る必要があります。

BeginRequest イベントに伴うデータ構造には、ポストバックを起動した DOM 要素への参照が含まれています。このメンバの名前は postBackElement で、次のようにアクセスします。

var lcPostbackElement = args.get_postBackElement();

この情報を使用すると、ユーザーがクリックした要素を確認し、無効にする必要がある要素を特定できます。

if (lcPostbackElement.id.toLowerCase() === "button1")
{
    $get("Button1").disabled = true;
    ...
}

元の UI を復元する際、EndRequest イベントにはこの情報が関連付けられません。よって、参照をキャッシュするか、要素の ID のみをキャッシュして、EndRequest イベント ハンドラ内から再度確認できるようにする必要があります。

DOM 要素を無効または有効にするには、要素への DOM 参照を取得した後、ブール型の disabled プロパティを操作する必要があります。ASP.NET AJAX の $get 関数は、DOM の document.getElementById 呼び出しと同等です。より正確に言えば、$get は、パラメータ化された getElementById 呼び出しと同等です。getElementById 呼び出しも、検索する DOM サブツリーのルートを指定するオプションの参照を受け取ります。

スクリプト サービス呼び出しで UI 要素を一時的に無効にするのは非常に簡単です。異なるのは、$get 関数を使用する場所だけです。図 3 に示すように、呼び出しを行う前に無効にし、成功時コールバックと失敗時コールバックで再度有効にします。

Figure 3 スクリプト サービス呼び出しで UI 要素を無効にする (Close Figure 3)
function startCall()
{
   $get("Button1").disabled = true;
   ...
   Samples.RemoteService.GetTime(param1, param2,
      onCompleted, onFailed);
}
function onCompleted(results)
{
   // Process response
   ...
   $get("Button1").disabled = false;
   ...
}
function onFailed(error)
{
   $get("Button1").disabled = false;
   ...
}

名前付けコンテナ内の UI 要素を無効にする

これまで説明したコードは特に問題なく機能しますが、まだまだ完成ではありません。マスタ ページやユーザー コントロールに埋め込まれた UI 要素を無効にする必要がある場合は、どうすればよいでしょうか。

多くの ASP.NET サーバー コントロールでは、名前付けコンテナを使用して、子コントロールの ID プロパティ値用に一意の名前空間を作成します。これは、GridView や Repeater などの、テンプレート化された反復的なデータ バインド コントロールの場合に特に重要です。各コントロールの名前付けコンテナは、INamingContainer インターフェイスを実装した最も近い親コントロールです。たとえば、UserControl クラスは、これに含まれているすべてのコントロールの名前付けコンテナとして機能します。マスタ ページに関連付けられたページ上の任意のコントロールに対しても同様です。

名前付けコンテナの管轄下にある ASP.NET コントロールは、そのクライアント側 ID の先頭に親の ID が付加されます。次のコードについて考えます。

<form runat="server">
  <asp:Button runat="server" ID="Button1" />
  ...
</form>

ボタンのクライアント ID はサーバー ID に一致し、常に Button1 です。ただし、ボタンをユーザー コントロールに移動したり、マスタ ページからコンテンツ ページを派生させたりすると、このプログラミング アスペクトが突然変化します。次のユーザー コントロールについて考えます。

<%@ Control Language="C#" CodeFile="test.ascx.cs" Inherits="test" %>
<asp:Button runat="server" ID="Button2" Text="Click" />

このユーザー コントロールを ASP.NET ページに埋め込んだ後は、生成される HTML が Button2 を参照します。

<input type="submit"
       name="UserControl1$Button2"
       value="Click"
       id="UserControl1_Button2" />

今度は、含まれている各コントロールの ID の先頭に、親の ID が付いています。サーバー側プログラミングに関する限り、この動作は開発者にとって実質的に透過的です。しかし、クライアントから Button2 のスクリプトを作成する必要がある場合は、名前付けコンテナと、ページ DOM 内の HTML 要素のクライアント ID の変化について知る必要があります。

図 4 では、図 2 と同じページが、マスタ ページのコンテンツ ページとして埋め込まれています。ここで、Button1 の実際のクライアント ID は次のとおりです。

Figure 4 ポストバック中に UI を無効にするコンテンツ ページ (Close Figure 4)
<%@ Page Language="C#" AutoEventWireup="true"
    CodeFile="DisableUIEmb.aspx.cs"
    MasterPageFile="~/Sample.master" Title="Disable UI Elements (Embedded)"
    Inherits="Samples_DisableUIEmb" Theme="ProgAjax1" %>

<asp:Content runat="server" ContentPlaceHolderID="ScriptPlaceHolder">
    <script type="text/javascript">
    function pageLoad()
    {
        var manager = Sys.WebForms.PageRequestManager.getInstance();
        manager.add_beginRequest(OnBeginRequest);
        manager.add_endRequest(OnEndRequest);
    }
    var lcPostbackElementID;
    function OnBeginRequest(sender, args)
    {
        lcPostbackElementID = args.get_postBackElement().id.toLowerCase();
        if (lcPostbackElementID === "<%= Button1.ClientID.ToLower() %>")
        {
            $get("<%= Button1.ClientID %>").disabled = true;
        }
    }

    function OnEndRequest(sender, args)
    {
        if (lcPostbackElementID === "<%= Button1.ClientID.ToLower() %>")
        {
            $get("<%= Button1.ClientID %>").disabled = false;
        }
    }
    </script>
</asp:Content>

<asp:Content runat="server" ContentPlaceHolderID="ContentPlaceHolder1">

    <div id="pageContent">

        <b>Enter some data:</b><br />
        <asp:TextBox ID="TextBox1" runat="server" />
        <asp:Button ID="Button1" runat="server" Text="Post ..."
            onclick="Button1_Click" />
        <br /><br />

        <asp:UpdateProgress runat="server" ID="UpdateProgress1">
            <ProgressTemplate>
                <img src="images/loading.gif" />
            </ProgressTemplate>
        </asp:UpdateProgress>

        <asp:UpdatePanel ID="UpdatePanel2" runat="server"
            UpdateMode="Conditional">
            <ContentTemplate>
                <asp:Label runat="server" ID="Label1" />
            </ContentTemplate>
            <Triggers>
                <asp:AsyncPostBackTrigger ControlID="Button1" />
            </Triggers>
        </asp:UpdatePanel>
    </div>

</asp:Content>

ctl00_ContentPlaceHolder1_Button1

Button1 を適切に無効にするには (また、一般にこれをスクリプト作成するには)、この新しい ID を知る必要があります。名前を調べて外部の JavaScript ファイルにハードコードすることはできますが、正確な ID は実行時にのみ判明します。図 4 は、ASP スタイルのコード ブロックとサーバー側の ClientID プロパティを使用して、名前付けコンテナに対処するパラメータ スクリプト コードをブラウザに出力する方法を示しています。図 5 は、実行中のサンプル ページです。

図 5 UI の調整にスクリプトを使用したコンテンツ ページ
図 5 UI の調整にスクリプトを使用したコンテンツ ページ(画像を縮小するには、ここをクリックします)
図 5 UI の調整にスクリプトを使用したコンテンツ ページ
図 5 UI の調整にスクリプトを使用したコンテンツ ページ (画像を拡大するには、ここをクリックします)

UpdatePanelAnimation エクステンダを使用する

無効にする UI 部分はポストバック固有であり、通常はプレーンなスクリプトを使用して定義します。ほとんどの場合、要素をグレーアウトするだけで十分ですが、それ以上のことも可能です。たとえば、インターフェイスの一部をフェード アウトしてから、再度フェード インすることもできます。

スクリプト サービス アプローチを使用する場合、こうした高度なユーザー操作の実装は、全面的に開発者の役割です。ただし、部分的なレンダリング アプローチを使用し、AJAX Control Toolkit をインストールした場合は、非常に便利なエクステンダ コントロール UpdatePanelAnimation エクステンダを利用できます。

エクステンダ コントロールとは、さまざまな既存のサーバー コントロールに動作を追加する、ASP.NET サーバー コントロールです。コントロールに特別な動作が必要な場合、その動作をエクステンダにコーディングした後、エクステンダと元のコントロールを同じページでバインドします。たとえば、テキスト ボックス内の数字以外の文字をフィルタで除去する場合を考えます。アドホックな TextBox コントロールを開発するか、従来のプレーンな TextBox コントロールを使用して、適切な動作で拡張できます。同じ動作を複数の種類のコントロールに再利用できるという利点、さらには、複数の動作を自由に組み合わせてサーバー コントロールの特定のインスタンスを拡張できるという利点もあります。

UpdatePanelAnimation エクステンダは、ASP.NET AJAX の部分的なレンダリング ページ内にある任意の更新可能な領域に、アニメーションを追加するよう設計されています。ポストバックの進行中にページで使用するアニメーションを、宣言によって関連付けます。このエクステンダは、AJAX Control Toolkit を通して入手できるスクリプトベースのアニメーション フレームワークと連動します。図 6 に、更新可能な領域をアニメーション処理するためのサンプル コードを示します。

Figure 6 UpdatePanelAnimation エクステンダを使用する (Close Figure 6)
<asp:UpdatePanel ID="UpdatePanel1" runat="server"
    UpdateMode="Conditional">
     <ContentTemplate>
         <asp:Button ID="Button1" runat="server" Text="Load" ... />
         <div id="Panel1">
            ...
         </div>
     </ContentTemplate>
     <Triggers>
         ...
     </Triggers>
</asp:UpdatePanel>

<act:UpdatePanelAnimationExtender ID="UpdatePanelAnimation1" runat="server"
    TargetControlID="UpdatePanel1">
    <Animations>
        <OnUpdating>
            <Sequence>
                <EnableAction AnimationTarget="Button1" Enabled="false" />
                <FadeOut AnimationTarget="Panel1" minimumOpacity=".3" />
            </Sequence>
        </OnUpdating>
        <OnUpdated>
            <Sequence>
                <FadeIn AnimationTarget="Panel1" minimumOpacity=".3" />
                <EnableAction AnimationTarget="Button1" Enabled="true" />
            </Sequence>
        </OnUpdated>
    </Animations>
</act:UpdatePanelAnimationExtender>

まず、ページ内の特定の UpdatePanel コントロールにエクステンダをバインドし、次にアニメーションを構成します。図 6 のコードでは、ポストバックの発生前に 2 つの連続するアクションを (OnUpdating タグ)、ポストバックの完了時に別の 2 つの連続するアクションを (OnUpdated タグ)、それぞれ設定しています。EnableAction、FadeOut、および FadeIn は、いずれも AJAX Control Toolkit でサポートされている定義済みのアニメーションです。FadeOut と FadeIn は、CSS スタイルを使用して、適用先の DOM サブツリー (この例では Panel1) をフェード アウトおよびフェード インします。EnableAction アニメーションはただ、指定されたターゲットの DOM 要素を無効または有効にします。

直接スクリプトを作成する方法に比べて、UpdatePanelAnimation エクステンダは宣言型プログラミングをサポートするため、要素のグレーアウトにとどまらない可能性を提供します。ただし、このエクステンダは、部分的なレンダリングを使用するページでのみ使用できます。詳細については、AJAX Control Toolkit のホーム ページ (codeplex.com/AtlasControlToolkit) を参照してください。

ページ全体を無効にする

既に述べたとおり、無効にする UI 部分は、ユーザーがクリックしたコントロールによって、さらに一般的に言えば、必要なアクションによって異なります。ところが、部分的なレンダリングのコンテキストにおいて、一度に実行できるのは 1 つの要求だけです。保留中の操作によって影響を受けるインターフェイス部分だけを特定して無効にする方法は、作業が煩雑であるばかりか、望ましくないクリック操作を防ぐこともできません。この点については後ほど詳しく説明します。今のところは、ページ全体を無効にするほうが、グレーアウトすべきコントロールをわざわざ特定する必要もなく、より簡単で効果的な解決策であるとだけ述べておきましょう。

考え方としては、ブラウザのクライアント領域と同じ大きさの DIV タグを作成し、これを使用してページ内にあるすべてを隠します。このようにすると、クリックしてもこの DIV によってキャプチャされ、その下にある HTML 要素には届きません。さらに、DIV をスタイルに関連付けて、操作の処理中にページをグレーアウトすることもできます。図 7 に、このタスクを行うための JavaScript コードを示します。

Figure 7 DIV で UI を隠す (Close Figure 7)
<script type="text/javascript">
    var _backgroundElement = document.createElement("div");

    function pageLoad()
    {
        var manager = Sys.WebForms.PageRequestManager.getInstance();
        manager.add_beginRequest(OnBeginRequest);
        manager.add_endRequest(OnEndRequest);

    // pageContent is the parent of the new DIV
        $get("pageContent").appendChild(_backgroundElement);
    }

    function OnBeginRequest(sender, args)
    {
        EnableUI(false);
    }

    function OnEndRequest(sender, args)
    {
        EnableUI(true);
    }

    function EnableUI(state)
    {
        if (!state)
        {
            _backgroundElement.style.display = '';
            _backgroundElement.style.position = 'fixed';
            _backgroundElement.style.left = '0px';
            _backgroundElement.style.top = '0px';

            var clientBounds = this._getClientBounds();
            var clientWidth = clientBounds.width;
            var clientHeight = clientBounds.height;
            _backgroundElement.style.width =
               Math.max(Math.max(document.documentElement.scrollWidth,
               document.body.scrollWidth), clientWidth)+'px';
            _backgroundElement.style.height =
               Math.max(Math.max(document.documentElement.scrollHeight,
               document.body.scrollHeight), clientHeight)+'px';
            _backgroundElement.style.zIndex = 10000;
            _backgroundElement.className = "modalBackground";
        }
        else
        {
            _backgroundElement.style.display = 'none';
        }
    }

    function _getClientBounds()
    {
        var clientWidth;
        var clientHeight;
        switch(Sys.Browser.agent) {
            case Sys.Browser.InternetExplorer:
                clientWidth = document.documentElement.clientWidth;
                clientHeight = document.documentElement.clientHeight;
                break;
            case Sys.Browser.Safari:
                clientWidth = window.innerWidth;
                clientHeight = window.innerHeight;
                break;
            case Sys.Browser.Opera:
                clientWidth = Math.min(window.innerWidth,
                              document.body.clientWidth);
                clientHeight = Math.min(window.innerHeight,
                               document.body.clientHeight);
                break;
            default:  // Sys.Browser.Firefox, etc.
                clientWidth = Math.min(window.innerWidth,
                              document.documentElement.clientWidth);
                clientHeight = Math.min(window.innerHeight,
                               document.documentElement.clientHeight);
                break;
        }
        return new Sys.UI.Bounds(0, 0, clientWidth, clientHeight);
    }
</script>

DIV タグを表すグローバルな JavaScript 変数が宣言されます。DIV は初期化された後、指定した場所の DOM ツリーに追加されます。ここでは、表示可能なインターフェイス全体をラップする DIV タグの子として定義されています。BeginRequest ハンドラによって DIV が構成および表示されるため、表示可能なインターフェイスが無効になります。EndRequest ハンドラはただ DIV を非表示にし、その結果 UI が復元されます。

function OnBeginRequest(sender, args)
{
    EnableUI(false);
}

function OnEndRequest(sender, args)
{
    EnableUI(true);
}

DIV タグは 0,0 の位置に固定され、その幅と高さは、ブラウザ固有の方法で動的に計算されます。DIV のサイズには、ブラウザのクライアント領域の現在のサイズが反映されます。getClientBounds 関数は、さまざまなブラウザのオブジェクト モデルにおける違いに対処するスクリプト プログラミングの例を示しています。

ご覧のとおり、このコードでは、Microsoft® AJAX クライアント ライブラリで構築されたクラスを使用しています。このコードは、AJAX Control Toolkit の非常に便利なエクステンダの 1 つである ModalPopup エクステンダのソース コードから借用しました。特に、ModalPopup エクステンダを使用すると、ページのコンテンツをユーザーに対してモーダルに表示できます。つまりユーザーは、残りのページを操作できなくなります。効果としては window.alert と同じですが、インターフェイス (表示するコントロールの階層) を自由に決めることができます。

図 8 に、モーダルな DIV を使用して、部分的なレンダリング操作中に UI 全体を無効にするページを示します。

図 8 部分的なレンダリング中にページ全体を無効にする
図 8 部分的なレンダリング中にページ全体を無効にする(画像を縮小するには、ここをクリックします)
図 8 部分的なレンダリング中にページ全体を無効にする
図 8 部分的なレンダリング中にページ全体を無効にする (画像を拡大するには、ここをクリックします)

一度に 1 つの AJAX ポストバック

部分的なレンダリングを使用すると、プログラミング モデルを変更することなく従来の ASP.NET 2.0 ページに AJAX 機能を追加できるため、ページのサーバー ライフサイクルが完全に維持されます。これは、ポストバック イベント、読み込み、レンダリング、およびビュー ステートに当てはまります。部分的なレンダリングの通常の呼び出しのトラフィック量は、従来の ASP.NET 呼び出しのトラフィック量よりも少ないですが、ビュー ステートのサイズは定数で、ページ固有のプログラミング手法によってのみ小さくできます。

部分的なレンダリングでは、ASP.NET ページに AJAX ページの連続感が加わりますが、ユーザー インターフェイスは従来のままです。クライアント側にとって、部分的なレンダリング ページは、1 つの連続した UI の一部であるかのように見えます。一度に保留できる操作は 1 つだけです。これは、たとえビジネスの観点から 2 つの同時操作が可能な場合でも同様です。

顧客に関するさまざまな情報を表示するページがあるとします。このページにはボタンが 2 つ含まれています。1 つは注文情報をダウンロードするボタン、もう 1 つは顧客の詳細を表示するボタンです。純粋にビジネスの視点からは、ユーザーが 2 つのボタンを順にクリックすることによって 2 つの要求を連続して生成し、最初の要求が完了する前に 2 番目の要求が開始されたとしても、まったく問題ありません。2 つの要求は同じ入力 (顧客 ID) を必要とし、どちらも読み取り専用であるため、バックエンド データベースの状態が干渉される危険はありません。しかしながら、部分的なレンダリング ページでは、これら 2 つの呼び出しを同時に実行することができません。部分的なレンダリングは従来の ASP.NET プログラミング モデルに基づいており、各要求から返されるマークアップに、ページ全体の更新されたビュー ステートが含まれるためです。2 つの要求の同時実行を許可すると、ページのビュー ステートの一貫性を危険にさらすことになります。2 つの要求を立て続けに送信すると、同じビュー ステートがサーバーに送信されますが、それぞれの要求から返されるビュー ステートは、要求の処理にかかわるコントロールが更新されているため、互いに異なります。これは、最初の要求が返されたときに加えられた変更が、最後に返される要求によって上書きされることを意味します。このため、部分的なレンダリングの AJAX ページは、今でもストップ アンド ゴーのパターンに従って機能しています。

この種の問題を回避するため、ページ要求マネージャ オブジェクトは、新しい要求の実行時に保留中の要求があれば自動的にこれらを終了します。この動作を考慮すると、部分的なレンダリング ページでは UI 全体を無効にすることが非常に重要です。こうすると、現在の要求が継続している間、ユーザーがページ全体を操作できなくなります。お気付きのとおり、この説明は、AJAX の主要な利点の 1 つである非同期操作とまったく相反するものです。

優先度の高い呼び出し

部分的なレンダリングを検討していくと、いくつかの疑問が生じます。たとえば、ビュー ステートの一貫性を保つ必要性のために 2 つの要求を同時に実行できないならば、2 つの部分的なビュー ステートをクライアント上でマージするアルゴリズムをなぜ実装しなかったのか、という疑問です。これは筋の通った疑問のように思われます。しかし、これが実現しなかった (少なくとも部分的な) 理由は、エンコードされたコンテンツ、ハッシュ値、およびサーバーのみのセキュリティ値が含まれた、ビュー ステートの内部構造にあります。

クライアント上で 2 つのビュー ステートを組み合わせることは、技術的には可能です。ただし、そのためには、ビュー ステートの現在の構造および ASP.NET ランタイムに対する大幅な変更が必要です。さらに、スクリプト コードを実質的にだれもが入手できるようにし、リプレイ攻撃やクロスサイト スクリプティング攻撃用のアドホックなビュー ステートの内容を作成できるようにすることによって、セキュリティ上の問題も発生します。

もう 1 つの一般的な疑問は、ページ要求マネージャに実装されている "最後を優先" ポリシー、つまり、新しい要求が受信されるたびに保留中の要求が終了される点に関してです。プログラミングに関して他人が決定を行うと、開発者は強制されたように感じがちです。ですから、私を含め多くの開発者が、AJAX ポストバックの "最後を優先" ポリシーの押し付けを好まないようです。では、基本的に現在の要求が優先され、後続の要求はキャンセルされるか最初の要求が完了するまでキューに格納されるような、"最初を優先" ポリシーならばどうでしょうか。

ASP.NET AJAX フレームワークには、このパターンを実装するために必要なツールが用意されています。これについては、この後ですぐに説明します。しかし、このオプションが本当に必要かどうか、もう一度確認してください。実際、部分的なレンダリングを使用する場合は、一度に 1 つの要求をページに配信することをお勧めします。そのための最も簡単で安全な方法は、UI 全体を無効にし、ユーザーがクリックできないようにすることです。

何かの理由でユーザー インターフェイス全体を無効にできない場合は、"最初を優先" アプローチの実装方法を知っておくと、必要な柔軟性を実現できる可能性があります。つまり、部分的なレンダリングの各要求の開始 (ページ要求マネージャの initializeRequest イベント) をインターセプトするスクリプト コードを記述します。ハンドラで実行時の条件を確認して、要求を本当にキャンセルするかどうかを決定し、それに応じてマネージャに指示します。

function OnInitializeRequest(sender, args)
{
    var manager = sender;
    if (manager.get_isInAsyncPostBack() &&
        args.get_postBackElement().id === "Button1")
    {
        args.set_cancel(true);
    }
}

サンプル コードでは、他の要求が処理中でないかどうかと、これが優先度の低い要求かどうかを確認します。そうであれば、イベント データ構造の cancel プロパティを true に設定することにより、要求をキャンセルできます。前のコードによれば、最新の要求はキャンセルされます。しかし、保留する要求用の内部キューを実装するコードを記述することもできます。優先度の高い呼び出しの詳細については、ajax.asp.net/docs/tutorials/ExclusiveAsyncPostback.aspx を参照してください。

ご意見やご感想は、

Dino cutting@microsoft.com.

まで英語でお送りください。
Dino Esposito は、Solid Quality Learning の指導者であり、『Introducing ASP.NET AJAX』 (Microsoft Press、2007 年) の著者でもあります。Dino はイタリアに在住し、世界各地で開催される業界のイベントで頻繁に講演しています。連絡先は、cutting@microsoft.com (英語) です。また、weblogs.asp.net/despos (英語) のブログにも参加してみてください。


チーム システム: カスタム チェックイン ポリシー

チーム システム
カスタム チェックイン ポリシー
Brian A. Randell

コードのダウンロード : : TeamSystem2007_11.exe (166KB)
コンテンツ
チェックイン メモおよびポリシー
独自のポリシーを作成する
ポリシー アセンブリをデバッグする
カスタム ポリシーを拡張する
編集サポートを追加する

このコラムの過去 3 つの記事では、Team Foundation Server (TFS) のバージョン管理 API と作業項目トラッキング API を紹介しました。Word 文書のチェックインと作業項目の関連付けをサポートする Microsoft® Word 2003 アドインをビルドする API を使用しましたが、これは Visual Studio® 2005 のチーム エクスプローラから使用できる機能に似ていました。このコラムではチェックイン メモとポリシーを詳しく掘り下げます。チェックイン メモの機能と、カスタム ポリシー実装を記述する方法について理解できることでしょう。この後のコラムで、このサポートを Word アドインに追加します。

チェックイン メモおよびポリシー

チェックイン メモとは、分類された文字列データをチェックインに追加できるようにする自由な形式のテキスト フィールドです。チェックイン メモは、チーム プロジェクト レベルで定義され、チェックイン操作の一環として必須にすることができます。マイクロソフトが提供する既定のプロジェクト テンプレートでは、Code Reviewer、Security Reviewer、Performance Reviewer という 3 つのチェックイン メモを定義しています。いずれも必須ではありません。チーム プロジェクトの作成後にチーム エクスプローラまたは API を使用するか、チーム プロジェクトの作成前にプロセス テンプレートの VersionControl.xml ファイルを変更すると、独自のチェックイン メモを定義できます。チェックイン メモを定義するには、64 文字までのラベルを定義し、チェックイン時にメモが必要かどうかを指定し、チェックイン フォーム上でメモを表示する位置を指定します。チェックイン時、メモの値は最大 2048 + (230 – 1) 文字です。最初の 2048 文字は、データベースの nvarchar 列に格納されます。これを超えた分は ntext 列に格納されます。

チェックイン ポリシーは、チーム プロジェクト管理者が定義したルールを適用します。ポリシーは、チェックイン時に必ずコメントを入力するなど簡単なものから、静的コード分析を行ってテストを実行するなど複雑なものまで指定できます。TFS の 2005 リリースには、3 つのポリシーが組み込まれていました。その後、Microsoft Visual Studio 2005 Team Foundation Server Power Tool の一部として、4 つのチェックイン ポリシーが追加されました。図 1 に 7 つのチェックイン ポリシーの詳細を示します (ここは "Tools" ではなく "Tool" です。msdn2.microsoft.com/aa718351 を参照してください)。

Figure 1 マイクロソフト提供のポリシー (Close Figure 1)

ポリシー | リリース | ポリシー要件
作業項目 | RTM | チェックインの少なくとも 1 つの作業項目を関連付けます。設定オプションはありません。
コード分析 | RTM | チェックイン前に静的コード分析 (クリーン コンパイルを意味する) を実行します。ポリシーをマネージ コードと C++ コードのどちらに適用するか制御します。
テスト | RTM | チェックイン前に、ポリシーのテスト一覧で指定されたテストを実行します。ポリシーが実行するテストの一覧を制御します。
コメントの関連付け | Power Tool | チェックイン時にコメントを入力します。設定オプションはありません。
禁止されたパターン | Power Tool | リポジトリにチェックインしない一連のファイルを定義できます。単にファイル拡張子を指定するか、正規表現を使用してポリシーを設定します。
作業項目のクエリ | Power Tool | 指定されたクエリの作業項目とチェックインを関連付けます。チーム プロジェクトからチーム クエリを選択して、ポリシーを設定します。
カスタム パス | Power Tool | 別のポリシーと連携します。チーム プロジェクトのソース コード管理ツリーのサブセットに "連係" ポリシーを選択的に適用できます。

ポリシーを実装するには、既知の 2 つのインターフェイス IPolicyDefinition および IPolicyEvaluation を実装する、シリアル化可能なパブリック クラスを公開するマネージ クラス ライブラリを作成します。ポリシー アセンブリをインストールするには、まず、チーム エクスプローラのコンポーネントがインストールされているワークステーションにコピーします。次に、Windows® レジストリの HKEY_LOCAL_ MACHINE\SOFTWARE\Microsoft\VisualStudio\8.0\TeamFoundation\SourceControl\Checkin Policies にエントリを追加して、アセンブリを登録します。[名前] には拡張子 .dll を除いたポリシー アセンブリのファイル名を、[データ] にはローカル ワークステーション上のアセンブリへの完全修飾パスを指定して、文字列値を作成します (図 2 参照)。

図 2 レジストリ キー
図 2 レジストリ キー(画像を縮小するには、ここをクリックします)
図 2 レジストリ キー
図 2 レジストリ キー (画像を拡大するには、ここをクリックします)

ポリシーをインストールした後、通常はチーム エクスプローラの [Source Control Settings] (ソース管理の設定) ダイアログで、各チーム プロジェクトに対してポリシーを有効にします。このダイアログにアクセスするには、まず、チーム エクスプローラ ツールのウィンドウから有効なチーム プロジェクトを選択し、[チーム] の [Team Project Settings] (チーム プロジェクトの設定) の [ソース管理] に移動し、[Check-in Policy] (チェックイン ポリシー) タブをクリックします (図 3 参照)。

図 3 [Check-in Policy] (チェックイン ポリシー) タブ
図 3 [Check-in Policy] (チェックイン ポリシー) タブ(画像を縮小するには、ここをクリックします)
図 3 [Check-in Policy] (チェックイン ポリシー) タブ
図 3 [Check-in Policy] (チェックイン ポリシー) タブ (画像を拡大するには、ここをクリックします)

最初に [Source Control Settings] (ソース管理の設定)] ダイアログから [追加] ボタンを選択する際に、チーム エクスプローラではローカル ワークステーション上の登録されたアセンブリの一覧が列挙されます (この後は実際には、[Source Control Settings] (ソース管理の設定) ダイアログを開くたびに一覧が列挙されます)。[追加] ボタンをクリックすると、チーム エクスプローラではインストール済みのポリシー アセンブリの一覧と選択したポリシーの説明が表示されます (図 4 参照)。

図 4 [Add Check-In Policy] (チェックイン ポリシーの追加) ダイアログ
図 4 [Add Check-In Policy] (チェックイン ポリシーの追加) ダイアログ

[Add Check-In Policy] (チェックイン ポリシーの追加) ダイアログからチェックイン ポリシーを選択すると、チーム エクスプローラはそのポリシーの IPolicyDefinition 実装の CanEdit プロパティを照会します。CanEdit が true を返す場合、チーム エクスプローラはポリシーの Edit メソッドを呼び出し、ポリシーが追加設定用のダイアログを表示できるようにします。ポリシーを設定し [Source Control Settings] (ソース管理の設定) ダイアログを終了した後、チーム エクスプローラは TeamProject オブジェクトのインスタンスから SetCheckinPolicies メソッドを呼び出し、ポリシー アセンブリに関するメタデータとポリシー クラスのインスタンスをシリアル化して Team Foundation Server データベースに格納します。

ポリシー アセンブリは、特定のチーム プロジェクト用に有効にするまでは TFS で認識しないので注意が必要です。ポリシーを有効にした後でさらに、チェックインを行うすべての開発者のワークステーション上にポリシー アセンブリをインストールする必要があります。現在、TFS には自動の展開メカニズムは用意されていません。

独自のポリシーを作成する

独自のポリシーを作成するには、Microsoft .NET Framework 2.0 とチーム エクスプローラをインストールしておく必要があります。Visual Studio 2005 をインストールしなくてもアセンブリは作成できますが、ここでは少なくとも Visual Studio 2005 Professional Edition の使用を想定しています。最初に、Visual Basic® のクラス ライブラリ プロジェクトを新たに作成します。この例として、.vb ファイルまたは .cs ファイルの不適切な語をスキャンするポリシー アセンブリを作成します。このアセンブリの名前を MSDNMag TeamSystem.CheckinPolicies とします。プロジェクトの作成後、ファイル名を Class1.vb から BadWords.vb に変更し、クラス Serializable をマークします。

Microsoft.TeamFoundation.VersionControl.Client.dll アセンブリへの参照を追加する必要があります。このアセンブリは Visual Studio 2005 SDK に付随してインストールされ、IPolicyDefinition および IPolicyEvaluation の定義を含んでいます。さらに、より迅速に作業を始められる抽象的な基本クラス PolicyBase があります。ただし、この例では、独自のクラスにインターフェイスを一度に 1 つずつ明示的に実装します。

BadWords.vb クラス ファイルに Microsoft.TeamFoundation.VersionControl.Client の imports ステートメントを追加します。次に IPolicyDefinition インターフェイスを実装します。Visual Studio 2005 では、このインターフェイスの 6 つのメンバを消します。図 5 はこのインターフェイスの実装を示しています。

Figure 5 BadWords ポリシーの IPolicyDefinition (Close Figure 5)
Imports Microsoft.TeamFoundation.VersionControl.Client

<Serializable()> _
Public Class BadWords
  Implements IPolicyDefinition

  Public ReadOnly Property CanEdit() As Boolean _
    Implements IPolicyDefinition.CanEdit
    Get
      Return False
    End Get
  End Property

  Public ReadOnly Property Description() As String _
    Implements IPolicyDefinition.Description
    Get
      Return "Checks code for bad words."
    End Get
  End Property

  Public Function Edit(ByVal policyEditArgs As IPolicyEditArgs) _
    As Boolean Implements IPolicyDefinition.Edit

    Throw New NotImplementedException("Edit is not implemented")
  End Function

  Public ReadOnly Property InstallationInstructions() As String _
    Implements IPolicyDefinition.InstallationInstructions
    Get
      Using sw As New IO.StringWriter()
        With sw
          .Write("You need to install ")
          .Write("BrianRandell.MSDNMag.CheckinPolicies.dll ")
          .Write("on your local workstation." & .NewLine())
          .WriteLine("Visit the Team Project for more info.")
        End With

        Return sw.ToString()
      End Using
    End Get
  End Property

  Public ReadOnly Property Type() As String _
      Implements IPolicyDefinition.Type
    Get
      Return "Brian Randell-MSDNMag Check for Bad Words""
    End Get
  End Property

  Public ReadOnly Property TypeDescription() As String _
    Implements IPolicyDefinition.TypeDescription
    Get
      Return "This policy checks .vb and .cs files for bad words."
    End Get
  End Property
End Class

図 5 のコードの大部分は説明不要でしょう。このインターフェイスから、ポリシーを説明する文字列値を入力したり、追加設定をサポートするポリシーを加える場合に必要となるフック CanEdit および Edit を指定したりします。Type プロパティは、[Add Check-In Policy] (チェックイン ポリシーの追加) ダイアログに表示される表示文字列を表しています。まず最初に、不適切な語の一覧をハードコードする必要があります。次に、CanEdit メソッドと Edit メソッドを実装します。

IPolicyDefinition を実装した後で、IPolicyEvaluation の実装が必要です。このインターフェイスでは、4 つのメソッド (Activate、DisplayHelp、Evaluate、および Initialize) と 1 つのイベント (PolicyStateChanged) を定義します。ポリシー評価フレームワークでは、ポリシー クラス インスタンスが作成された後に Initialize が呼び出されます。このメソッドは、保留中の変更や作業項目など、保留中のチェックインに関連するデータへのアクセス権を持つポリシー アセンブリを提供する IPendingCheckin 参照を受け取ります。Evaluate はポリシー アセンブリの中核です。チェックイン フレームワークは、ユーザーが [Policies] (ポリシー) ボタンをクリックするか、チェックインまたはシェルビングの操作を試みたときに、このメソッドを実行します。Evaluate が返す PolicyFailure インスタンスの一覧が空白でない場合、フレームワークはチェックインを取り消し、エラー一覧を表示します。エラー メッセージを選択してダブルクリックすると、フレームワークが Activate を呼び出し、ポリシー クラスがエラーの詳細を表示できるようにします。最後に、エラーを選択して F1 キーを押すと、フレームワークが DisplayHelp メソッドを実行します。これにより、ヘルプ ファイルを表示したり、Web ページに移動したり、カスタム UI を表示したりできます。ポリシー クラスでは、変更が発生したので再度 Evaluate を呼び出す必要があることをフレームワークに通知するために PolicyStateChanged イベントを使用します。図 6 は IPolicyEvaluation の最初の実装を示しています。

Figure 6 BadWords ポリシーの IPolicyEvaluate (Close Figure 6)
Imports Microsoft.TeamFoundation.VersionControl.Client
Imports System.IO

<Serializable()> _
Public Class BadWords
  Implements IPolicyDefinition, IPolicyEvaluation

' IPolicyDefinition elided (see Figure 5)

  Private badWords() As String = {"Dag", "Darn", "Doh"}
  <NonSerialized()> _
  Private m_pendingCheckin As IPendingCheckin = Nothing

  Public Sub Activate(ByVal failure As PolicyFailure) _
    Implements IPolicyEvaluation.Activate
    ' Do Nothing for Now
  End Sub

  Public Sub DisplayHelp(ByVal failure As PolicyFailure) _
    Implements IPolicyEvaluation.DisplayHelp
    ' Do Nothing for Now
  End Sub

  Public Function Evaluate() As PolicyFailure() _
    Implements IPolicyEvaluation.Evaluate

    Dim fileContents As String = Nothing
    Dim failureMessage As String = Nothing
    Dim failures As New List(Of PolicyFailure)

    For Each pc As PendingChange In _
      m_pendingCheckin.PendingChanges.CheckedPendingChanges

      ' Only check .vb and .cs files
      If pc.FileName.EndsWith(".vb") OrElse _
        pc.FileName.EndsWith(".cs") Then

        Using sr As New StreamReader(pc.FileName)
          fileContents = sr.ReadToEnd()
        End Using
        For Each s As String In badWords
          If fileContents.Contains(s) Then
            failureMessage = pc.FileName & " has a bad word."
            Dim pf As New PolicyFailure(failureMessage, Me)
            failures.Add(pf)
            Exit For
          End If
        Next
      End If
    Next

    Return failures.ToArray()
  End Function

  Public Sub Initialize(ByVal pendingCheckin As IPendingCheckin) _
    Implements IPolicyEvaluation.Initialize

    If (Not Me.m_pendingCheckin Is Nothing) Then
      Throw New InvalidOperationException("Already Active")
    End If
    If disposedValue Then
      Throw New ObjectDisposedException(Nothing)
    End If

    Me.m_pendingCheckin = pendingCheckin
  End Sub

  Public Event PolicyStateChanged(ByVal sender As Object, _
    ByVal e As PolicyStateChangedEventArgs) _
    Implements IPolicyEvaluation.PolicyStateChanged

  ' IDisposable implementation elided

End Class

最初の実装は非常に単純です。ポリシー インスタンスは、Initialize メソッドを介して提供される IPendingCheckin 参照への参照を格納します。Evaluate メソッドでは、コードが CheckedPendingChanges コレクションを列挙し、チェックされた各項目に対応する PendingChange インスタンスを取得します。項目が拡張子 .vb または .cs を持つファイルの場合、コードは StreamReader を使用してファイルの内容を文字列変数に読み込みます。次に、不適切な語のそれぞれについて一度に 1 つずつ、String.Contains メソッドでファイルの内容をスキャンします。不適切な語を検出すると、PolicyFailure インスタンスを新たに生成し、エラー List(Of T) コレクションに追加して、保留中の次の変更に移ります。すべてのファイルのスキャンを終えると、Evaluate を呼び出したポリシー フレームワークにエラーのコレクションを返します。

IPolicyEvaluation のサポートに必要なコードを追加した後で、アセンブリのコンパイルが必要です。必要なデータをレジストリに追加する必要があるので、[Source Code Control Settings] (ソース コード管理の設定) ダイアログで [追加] ボタンをクリックすると、Visual Studio がアセンブリを読み込みます。インストールされたポリシーの一覧を変更する場合、変更を反映するためには実行中の Visual Studio のインスタンスの再起動が必要になります。すべての設定を完了したら、ポリシーを有効にし、不適切な語を入力して、ソース管理下に置くファイルを変更します。次に、[Pending Changes] (保留中の変更) ダイアログを開き、[Policy Warnings] (ポリシー警告) ボタンをクリックするかチェックインを試行します。図 7 のような画面が表示されます。

図 7 ポリシー違反警告
図 7 ポリシー違反警告(画像を縮小するには、ここをクリックします)
図 7 ポリシー違反警告
図 7 ポリシー違反警告 (画像を拡大するには、ここをクリックします)

ポリシー アセンブリをデバッグする

カスタム ポリシーのデバッグは、実際には非常に簡単です。まず、カスタム ポリシーを有効にしている場合、そのポリシーをチーム プロジェクトの一覧から削除します。プライベートなチーム プロジェクトのポリシー アセンブリのみを対象にすると、作業が簡単になります。もちろん、ある時点で、有効にする選択をした他のポリシーと組み合わせてポリシー アセンブリをテストする必要がありますが、デバッグ中に問題の対象領域を小さくすることは合理的です。すべてのポリシーを削除した後で、実行中の Visual Studio 2005 のインスタンスをすべてシャットダウンし、以前にレジストリに追加したデータはそのままにします。ここで、Visual Studio 2005 のインスタンスの 1 つを再起動します。カスタム ポリシー プロジェクトを読み込み、プロジェクト デザイナにアクセスします (ソリューション エクスプローラでプロジェクトを右クリックして [プロパティ] をクリックします)。[デバッグ] タブをアクティブ化し、[開始動作] を [外部プログラムの開始] に変更します。Visual Studio 2005 の完全修飾パスを指定する必要があります。通常は C:\Program Files\Microsoft Visual Studio 8\Common7\IDE\devenv.exe です。変更を保存してデザイナを閉じます。

この後、IPolicyDefinition.Type と IPolicyEvaluate.Evaluate のブレークポイントを設定します。デバッグを開始するには、F5 キーを押すか、メニューから [デバッグ] の [Start Debugging] (デバッグ開始) をクリックします。Visual Studio の 2 つ目のインスタンスが開始され、このインスタンスで、不適切な語のテストに使用するプロジェクトが開かれます。次に [Source Control Settings] (ソース管理の設定) ダイアログにアクセスし、カスタム ポリシーを追加します。[追加] ボタンをクリックすると、Visual Studio の最初のインスタンスがアクティブ ウィンドウになり、Visual Studio はポリシーの Type プロパティにアクセスします。設定のダイアログを閉じると、ポリシー フレームワークによってポリシーの Evaluate メソッドが実行されます。その結果、Visual Studio の最初のインスタンスが再度アクティブ化され、デバッグを開始できます。デバッグ セッションが終わると、[Source Control Settings] (ソース管理の設定) ダイアログに戻り、ポリシーが削除されます。この時点で IDisposable 実装をデバッグすることができます。

処理が完了したら、Visual Studio の 2 番目のインスタンスを終了します。この時点で、ポリシー アセンブリを変更し、プロセスを繰り返すことができます。Visual Studio の 2 番目のインスタンスのポリシーを有効にするだけです。それ以外の場合は、すべてのインスタンスをアンロードして再コンパイルする必要があります。

カスタム ポリシーを拡張する

カスタム ポリシーの現在の実装は動作はしますが、あまり機能豊富とはいえません。不適切な語がどこにあるかという詳細な情報が示されません。さらに、不適切な語の一覧をハードコードしています。これを修正するには、IPolicyEvaluate.Evaluate の実装の機能を拡張する必要があります。その次に Activate メソッドの実際の実装を指定します。最後に、IPolicyDefinition.Edit の実装を指定します。図 8 は、Evaluate の修正後のコードを示しています。

Figure 8 更新された Evaluate の実装 (Close Figure 8)
Imports Microsoft.TeamFoundation.VersionControl.Client
Imports System.IO

<Serializable()> _
Public Class BadWords
  Implements IPolicyDefinition, IPolicyEvaluation

  Private badWords() As String = {"Dag", "Darn", "Doh"}
  <NonSerialized()> _
  Private m_pendingCheckin As IPendingCheckin = Nothing
  <NonSerialized()> _
  Private badWordFound As Boolean = False

  ' IPolicyDefinition elided
  ' IPolicyEvaluation Activate and DisplayHelp elided

  Public Function Evaluate() As PolicyFailure() _
    Implements IPolicyEvaluation.Evaluate

    Dim fileContents As String = Nothing
    Dim failureMessage As String = Nothing
    Dim wordData As List(Of WordInfo)
    Dim failures As New List(Of PolicyFailure)

    For Each pc As PendingChange In _
      m_pendingCheckin.PendingChanges.CheckedPendingChanges

      ' Only check .vb and .cs files
      If pc.FileName.EndsWith(".vb") OrElse _
        pc.FileName.EndsWith(".cs") Then

        badWordFound = False
        wordData = GetWordInfo(pc.LocalItem)
        If badWordFound Then
          failureMessage = _
            String.Format("{0} has at least one bad word." & _
            " Double-click for details", pc.FileName)

          Dim bpf As New BadWordPolicyFailure(failureMessage, _
            Me, New CheckedFileInfo(pc.FileName, wordData, pc))

          failures.Add(bpf)
        End If
      End If
    Next

    Return failures.ToArray()
  End Function

  ' IPolicyEvaluation Initialize elided
  ' IDisposable implenation elided

  Friend Function GetWordInfo(ByVal fileName As String) _
    As List(Of WordInfo)

    Dim lines As String() = File.ReadAllLines(fileName)
    Dim wordData As New List(Of WordInfo)

    For Each word As String In badWords
      Dim item As WordInfo = New WordInfo(word)
      wordData.Add(item)

      For i As Integer = 0 To lines.Length - 1
        If lines(i).Contains(word) Then
          item.LineList.Add(i + 1)
          item.LineData.Add(lines(i))
        End If
      Next i
      If item.LineList.Count > 0 Then
        badWordFound = True
      End If
    Next

    Return wordData
  End Function
End Class

Evaluate の新しい実装では、図 9 に示すように、一連の新しいヘルパ クラスを使用します。WordInfo は、不適切な語を含むコードの行番号と実際の行を保存します。不適切な語のそれぞれについて WordInfo のインスタンスが生成されます。CheckedFileInfo には、チェックされたファイルの名前、WordInfo オブジェクトのコレクション、PendingChange 項目への参照が格納されます。最後に、PolicyFailure から派生した BadWordPolicyFailure によって、CheckedFileInfo のインスタンスへの参照が保持されます。既定の PolicyFailure クラスでは、Activate メソッドが高機能なユーザー エクスペリエンスを実現するために必要となるサポート メタデータとポリシーのエラーを関連付ける簡単な方法が提供されないため、このクラスを作成しています。

Figure 9 Evaluate のヘルパ クラス (Close Figure 9)
Public Class WordInfo
  Public Word As String
  Public LineList As New List(Of Integer)
  Public LineData As New List(Of String)

  Public Sub New(ByVal word As String)
    Me.Word = word
  End Sub
  Public Overrides Function ToString() As String
    Return Word
  End Function
End Class

Public Class CheckedFileInfo
  Public FileName As String
  Public WordData As List(Of WordInfo)
  Public PendingItem As PendingChange

  Public Sub New(ByVal FileName As String, _
    ByVal WordData As List(Of WordInfo), _
    ByVal PendingItem As PendingChange)

    Me.FileName = FileName
    Me.WordData = WordData
    Me.PendingItem = PendingItem
  End Sub
End Class

Public Class BadWordPolicyFailure
  Inherits PolicyFailure

  Public FailureData As CheckedFileInfo

  Public Sub New(ByVal Message As String, _
    ByVal Policy As IPolicyEvaluation, _
    ByVal FailureData As CheckedFileInfo)

    MyBase.New(Message, Policy)
    Me.FailureData = FailureData
  End Sub
End Class

これらの新しいクラスを使用するので、新しい Evaluate メソッドは前と少し異なる動きをします。.vb および .cs ファイルのみを検索し、CheckedPendingChanges コレクションを列挙するのは同じです。しかし、GetWordInfo ヘルパ メソッドはソース ファイルを実際に読み込んで、不適切な語を検索します。このメソッドは、この一覧に基づいて不適切な語のそれぞれに対応する WordInfo で構成されるコレクションをビルドします。GetWordInfo が少なくとも 1 つの不適切な語を検出すると、Evaluate メソッドは CheckedFileInfo のインスタンスを生成し、エラーのコレクションに追加される BadWordPolicyFailure の新しいインスタンスに渡します。そして、各ファイルのチェックが続行されます。

次に Activate メソッドの実装を指定する必要があります。ポリシー違反メッセージをダブルクリックすると、その対象ポリシーについて不適切な語、検出した行、不適切な語を含む実際のコード行を一覧できるダイアログが表示されるようにします。サンプル コードのダウンロードでは、手始めに使用できる frmFileDetails という簡単なフォームを提供しています。

Activate メソッドの新しいコードを図 10 に示します。コードが最初に実行するのは、派生した BadWordPolicyFailure 型への PolicyFailure 参照として用意した TryCast です。この処理が正常に終了すると、frmFileDetails がダイアログとして表示されます (図 11 の例を参照)。

Figure 10 Activate メソッドの実装 (Close Figure 10)
Public Sub Activate(ByVal failure As PolicyFailure) _
  Implements IPolicyEvaluation.Activate

  Dim bpf As BadWordPolicyFailure = _
    TryCast(failure, BadWordPolicyFailure)

  If bpf IsNot Nothing Then
    Using frm As New frmFileDetails(bpf)
      frm.ShowDialog()
    End Using
  Else
    MsgBox("The Policy Failure object provided is invalid.", _
      MsgBoxStyle.Exclamation, "Warning")
  End If
End Sub
図 11 ポリシー エラー メッセージ
図 11 ポリシー エラー メッセージ(画像を縮小するには、ここをクリックします)
図 11 ポリシー エラー メッセージ
図 11 ポリシー エラー メッセージ (画像を拡大するには、ここをクリックします)

編集サポートを追加する

最後に追加する必要のある機能は、プロジェクト管理者が不適切な語の一覧を定義できるようにするダイアログです。ここでは 3 つのタスクを実行します。最初に、不適切な語の一覧を格納するメカニズムを用意する必要があります。次に、不適切な語の一覧を編集する UI を提供します。最後に、ハードコードされた配列ではなく、格納場所から不適切な語を読み込むようにポリシーを変更します。いずれのタスクも、ポリシー アセンブリに特有のものではありません。すべて標準的なコードです。実装の例をコード サンプルとして提供しています。サンプルでは、アドインと同じディレクトリにあるテキスト ファイルに不適切な語の一覧を格納します。編集時にプロジェクト管理者には不適切な語のファイルに対する読み取り/書き込みアクセス権が必要ですが、ポリシーのコンシューマには読み取りアクセス権のみで十分です。

この点で、チェックイン メモとポリシーは便利だといえます。マイクロソフトがチェックイン ポリシーに装備したフレームワークは多機能で柔軟性に富んでいます。今後は、インストール手順が改善されることを期待しています。このコラムの次回の記事では、カスタム コードと Word アドインからチェックイン メモとポリシーをプログラムで操作する方法について詳しく説明します。

ご意見やご質問は、

こちら mmvsts@microsoft.com.

まで英語でお送りください。
Brian A. Randell は、MCW Technologies, LLC のシニア コンサルタントです。マイクロソフト テクノロジについての講演や執筆を行っています。Microsoft MVP として、Pluralsight 社の Applied Team System コースの作成にも携わりました。mcwtech.com/cs/blogs/brianr (英語) にブログを公開しています。


基礎: WCF での同期コンテキスト

基礎
WCF での同期コンテキスト
Juval Lowy

コードのダウンロード : : Foundations2007_11.exe (241KB)
コンテンツ
.NET 同期コンテキストとは
WCF と同期コンテキスト
サービスのカスタム同期コンテキスト
スレッドの関係の同期コンテキスト
優先順位の処理
コールバックと同期コンテキスト
同期コンテキストを使用する理由

Windows® Communication Foundation (WCF) の有用な機能の 1 つは、Microsoft® .NET Framework の同期コンテキストに依存してサービス インスタンス (またはコールバック オブジェクト) の呼び出しをマーシャリングすることです。このメカニズムは、生産性重視の開発と強力な拡張性の両方を実現します。このコラムでは、同期コンテキストおよび WCF でのその用途について簡単に説明した後に、カスタム同期コンテキストを使用するために WCF を拡張する場合のさまざまなオプションを、プログラムと宣言の両方を使って説明します。カスタム同期コンテキストのメリットに加えて、高度な .NET プログラミングおよび WCF を拡張するテクニックも紹介します。

.NET 同期コンテキストとは

.NET Framework 2.0 では、同期コンテキストと呼ばれるあまり知られていない機能が導入されています。これは、System.Threading 名前空間の SynchronizationContext クラスで定義されます。

public delegate void SendOrPostCallback(object state);

public class SynchronizationContext
{
   public virtual void Post(SendOrPostCallback callback,object state);
   public virtual void Send(SendOrPostCallback callback,object state);
   public static void SetSynchronizationContext
(SynchronizationContext context);
   public static SynchronizationContext Current
   {get;}
   //More members
}

同期コンテキストは、スレッド ローカル記憶域 (TLS) に格納されます。.NET Framework 2.0 で作成されるすべてのスレッドは、SynchronizationContext の静的な Current プロパティから同期コンテキスト (アンビエント トランザクションに類似) を取得している可能性があります。現在のスレッドに同期コンテキストがない場合は、Current から null が返されることがあります。同期コンテキストは、呼び出し元スレッドでメソッドを実行できない場合に、呼び出し元スレッドと 1 つ以上のターゲット スレッドの間でメソッド呼び出しをやり取りするために使用されます。呼び出し元スレッドは、他のスレッド (1 つまたは複数) にマーシャリングするメソッドを SendOrPostCallback 型のデリゲートでラップし、それを Send メソッド (同期実行の場合) または Post メソッド (非同期実行の場合) に提供します。同期コンテキストは、静的メソッド SetSynchronizationContext を呼び出すことで、現在のスレッドに関連付けることができます。

同期コンテキストが最もよく使用されるのは、UI を更新する場合です。MFC から Windows フォーム、そして WPF に至る Windows のどのマルチスレッド テクノロジでも、ウィンドウを作成したスレッドのみが、そのメッセージを処理することでウィンドウを更新できます。この制約は、Windows メッセージ ループとスレッド メッセージング アーキテクチャが背後で使用されていることに関係します。このメッセージング アーキテクチャによって、マルチスレッド UI アプリケーションを開発する際に問題が生じます。つまり、時間のかかる操作を実行したりコールバックを受け取ったりする際に、UI がブロックされないようにする必要があります。この場合は当然ワーカー スレッドを使用する必要がありますが、ワーカー スレッドは UI スレッドではないので UI を直接更新できません。

.NET Framework 2.0 でのこの問題に対処するために、Windows フォーム ベースのコントロールまたはフォームのコンストラクタは、自身が実行しているスレッドに同期コンテキストがあるかどうかを確認し、ない場合は新しい同期コンテキスト (WindowsFormsSynchronizationContext) をアタッチします。この専用の同期コンテキストによって、Post または Send メソッドの呼び出しすべてが Windows メッセージに変換され、さらに UI スレッド メッセージ キューに投稿されて正しいスレッドで処理されるようになります。Windows フォームの同期コンテキストは、一般に使用される BackgroundWorker ヘルパ コントロールの基になるテクノロジです。

WCF と同期コンテキスト

既定では、すべての WCF サービス呼び出し (およびコールバック) が、I/O 完了スレッド プール内のスレッドで実行されます。このプールには既定で 1,000 個のスレッドがあり、そのどれもがアプリケーションの制御を受けません。たとえば、ユーザー インターフェイスを更新する必要があるサービスがあるとします。このサービスは、不適切なスレッドにあるために、フォーム、コントロール、またはウィンドウに直接アクセスできません。これに対処するために、ServiceBehaviorAttribute から次のように定義された UseSynchronizationContext プロパティが提供されます。

[AttributeUsage(AttributeTargets.Class)]
public sealed class ServiceBehaviorAttribute : ...
{
   public bool UseSynchronizationContext
   {get;set;}
   //More members
}

UseSynchronizationContext の既定値は true です。このサービスが使用する必要がある同期コンテキストを特定するために、WCF はホストを開いたスレッドを調べます。このスレッドに同期コンテキストがあり、UseSynchronizationContext が true に設定されている場合は、サービスに対するすべての呼び出しを WCF が自動的にその同期コンテキストにマーシャリングします。開発者が同期コンテキストを明示的に操作する必要はありません。WCF は、この処理を行うために各エンドポイントのディスパッチャに同期コンテキストへの参照を提供します。ディスパッチャは、この同期コンテキストを使用してすべての呼び出しをディスパッチします。

たとえば、MyService サービス (図 1 で定義) が MyForm フォーム上の UI を更新する必要があるとします。ホストはフォーム構築後に開かれるため、ホストを開くスレッドには既に同期コンテキストがあります。したがって、このサービスの呼び出しは、すべて自動的に正しい UI スレッドにマーシャリングされます。

Figure 1 UI 同期コンテキストの使用 (Close Figure 1)
[ServiceContract]
interface IMyContract
{...}

class MyService : IMyContract
{
   /* some code to update MyForm */
}

class MyForm : Form
{...}

static class Program
{
   static void Main()
   {
      Form form = new MyForm();//Sync context established here

      ServiceHost host = new ServiceHost(typeof(MyService));
      host.Open();

      Application.Run(form);

      host.Close();
   }
}

ホストを Main で開き、Windows フォーム デザイナで生成されたコードを使用する場合は、同期コンテキストがない状態でホストが開かれるため、同期コンテキストのメリットは得られません。

//No automatic use of synchronization context
static void Main()
{
   ServiceHost host = new ServiceHost(typeof(MyService));
   host.Open();

   Application.Run(new MyForm());//Synchronization context
                                 //established here

   host.Close();
}

サービスのカスタム同期コンテキスト

サービスまたはリソースによるカスタム同期コンテキストの利用には、2 つの局面があります。同期コンテキストの実装とそのインストールです。このコラム付属のソース コードには、次のように定義された ThreadPoolSynchronizer クラスが含まれています。

public class ThreadPoolSynchronizer : SynchronizationContext,IDisposable
{
   public ThreadPoolSynchronizer(uint poolSize);
   public ThreadPoolSynchronizer(uint poolSize,string poolName);

   public void Dispose();
   public void Close();
   public void Abort();
}

ThreadPoolSynchronizer は、すべての呼び出しをカスタム スレッド プールにマーシャリングします。そこではまず呼び出しがキューに登録され、その後に利用可能なスレッド上で多重化されます。プールのサイズは、構築パラメータとして指定されます。プールが最大サイズに達すると、スレッドが使用可能になるまで呼び出しがキューで保留されます。プール名を指定することもできます (この名前は、プール内のスレッド名のプレフィックスになります)。ThreadPoolSynchronizer を破棄または終了すると、プール内のすべてのスレッドが正常に終了します。つまり、関係するスレッドがそのタスクを完了した後に終了します。Abort メソッドを実行すると、すべてのスレッドが突然終了されるため、正常なシャットダウンが行われません。ThreadPoolSynchronizer を実装する処理は WCF と無関係なので、このコラムのコードには含めていません。基になるワーカー スレッドとその割り当てを制御してスループットを最大化する必要があるサーバー アプリケーションでは、従来からカスタム スレッド プールが使用されています。

サービスをカスタム スレッド プールに関連付けるには、ホストを開くスレッドに ThreadPoolSynchronizer を手動でアタッチします。

SynchronizationContext syncContext = new ThreadPoolSynchronizer(3);

SynchronizationContext.SetSynchronizationContext(syncContext);

using(syncContext as IDisposable)
{
   ServiceHost host = new ServiceHost(typeof(MyService));
   host.Open();
   /* Some blocking operations */
   host.Close();
}

このスレッド プールには、3 つのスレッドが用意されます。図 1 と同じサービス定義を使用すると、MyService とこの 3 つのスレッドの間に関係が作成されます。このサービスの呼び出しは、サービス同時実行モードまたはインスタンス モードに関係なく、またサービスがサポートするエンドポイントとコントラクトを問わず、すべてこれらのスレッドに送られます。

上記のコードには、サービスがホスティング コードに支配されるという問題があります。サービスを意図的にプールで実行する必要がある場合はどうすればいいでしょうか。宣言によって、スレッド プールをサービス定義の一部として適用するとうまくいきます。そこで私は ThreadPoolBehaviorAttribute を作成しました。

[AttributeUsage(AttributeTargets.Class)]
public class ThreadPoolBehaviorAttribute : Attribute,
                                           IContractBehavior,
                                           IServiceBehavior
{
   public ThreadPoolBehaviorAttribute(uint poolSize,Type serviceType);
   public ThreadPoolBehaviorAttribute(uint poolSize,Type serviceType,
                                      string poolName);
}

この属性はサービスに直接適用し、サービスの種類をコンストラクタ パラメータとして指定します。

[ThreadPoolBehavior(3,typeof(MyService))]
class MyService : IMyContract
{...}

この属性は、ThreadPoolSynchronizer のインスタンスを、サービスのエンドポイントのディスパッチャに提供します。ThreadPoolBehaviorAttribute を実装する際に重要なのは、ディスパッチャを同期コンテキストに接続する方法とタイミングを知ることです。これを容易にするために、ThreadPoolBehaviorAttribute は特殊な WCF 拡張性インターフェイス IContractBehavior をサポートしています。

public interface IContractBehavior
{
   void ApplyDispatchBehavior(ContractDescription description,
                              ServiceEndpoint endpoint,
                              DispatchRuntime dispatch);
   // More members
}

IContractBehavior をサポートする属性でサービスが修飾されていると、ホストを開いた後 (かつサービスに呼び出しを転送する前) に各サービス エンドポイントに対して WCF が ApplyDispatchBehavior メソッドを呼び出し、その際に DispatchRuntime パラメータを指定してこのメソッドがディスパッチャに作用するようにします。図 2 は、ThreadPoolBehaviorAttribute の実装を示しています。

Figure 2 ThreadPoolBehaviorAttribute の実装 (Close Figure 2)
[AttributeUsage(AttributeTargets.Class)]
public class ThreadPoolBehaviorAttribute : Attribute,IContractBehavior,
                                           IServiceBehavior
{
   //Store values in matching members
   protected string PoolName
   {get{...};set{...};}
   protected uint PoolSize
   {get{...};set{...};}
   protected Type ServiceType
   {get{...};set{...};}

   public ThreadPoolBehaviorAttribute(uint poolSize,Type serviceType)
: this(poolSize,serviceType,null)
   {}
   public ThreadPoolBehaviorAttribute(uint poolSize,Type serviceType,
       string poolName)
   {
      PoolName    = poolName;
      ServiceType = serviceType;
      PoolSize    = poolSize;
   }
   protected virtual ThreadPoolSynchronizer ProvideSynchronizer()
   {
      if(ThreadPoolHelper.HasSynchronizer(ServiceType) == false)
         return new ThreadPoolSynchronizer(PoolSize,PoolName);
      else
         return ThreadPoolHelper.GetSynchronizer(ServiceType);
   }

   void IContractBehavior.ApplyDispatchBehavior
(ContractDescription description,ServiceEndpoint endpoint,
            DispatchRuntime dispatch)
   {
      PoolName = PoolName ?? "Pool executing endpoints of " + ServiceType;

      ThreadPoolHelper.ApplyDispatchBehavior(ProvideSynchronizer(),
        PoolSize,ServiceType,PoolName,dispatchRuntime);
   }
   void IServiceBehavior.Validate(ServiceDescription description,
        ServiceHostBase serviceHostBase)
   {
      serviceHostBase.Closed += delegate
          {
              ThreadPoolHelper.CloseThreads(ServiceType);
          };
   }
   //Rest of the implementation
}
public static class ThreadPoolHelper
{
   static Dictionary<Type,ThreadPoolSynchronizer> m_Synchronizers =
       new Dictionary<Type,ThreadPoolSynchronizer>();

   [MethodImpl(MethodImplOptions.Synchronized)]
   internal static bool HasSynchronizer(Type type)
   {
      return m_Synchronizers.ContainsKey(type);
   }

   [MethodImpl(MethodImplOptions.Synchronized)]
   internal static ThreadPoolSynchronizer GetSynchronizer(Type type)
   {
      return m_Synchronizers[type];
   }
   [MethodImpl(MethodImplOptions.Synchronized)]
   internal static void ApplyDispatchBehavior(
ThreadPoolSynchronizer synchronizer,uint poolSize,Type type,
    string poolName,DispatchRuntime dispatch)
   {
      if(HasSynchronizer(type) == false)
      {
         m_Synchronizers[type] = synchronizer;
      }
      dispatch.SynchronizationContext = m_Synchronizers[type];
   }
   [MethodImpl(MethodImplOptions.Synchronized)]
   public static void CloseThreads(Type type)
   {
      if(m_Synchronizers.ContainsKey(type))
      {
         m_Synchronizers[type].Dispose();
         m_Synchronizers.Remove(type);
      }
   }
}

WCF カスタム ビヘイビア属性の実装は、実際のビヘイビアと切り離しておくのがベスト プラクティスです。属性ではイベント シーケンスの決定のみを行い、実際のビヘイビアはヘルパ クラスで指定するようにします。そうすることで、たとえばカスタム ホストによって個別にビヘイビアを使用できます。ThreadPoolBehaviorAttribute があまり処理を行わないのはこれが理由です。その作業のほとんどは、ThreadPoolHelper という静的ヘルパ クラスにデリゲートされます。ThreadPoolHelper クラスには、指定されたサービスの種類に既に同期コンテキストがあるかどうかを確認する HasSynchronizer メソッド、およびその種類に関連付けられた同期コンテキストを返す GetSynchronizer メソッドがあります。ThreadPoolBehaviorAttribute は、仮想 ProvideSynchronizer メソッド内でこの 2 つのメソッドを使用して、そのサービスの種類に対してプールが 1 度だけ作成されるようにします。このチェックが必要となるのは、ApplyDispatchBehavior が複数回 (実際にはエンドポイントごとに 1 回) 呼び出される可能性があるからです。ThreadPoolBehaviorAttribute は、IServiceBehavior を実装することから、カスタム サービス ビヘイビアでもあります。

public interface IServiceBehavior
{
   void Validate(ServiceDescription description,ServiceHostBase host);
   //More members
}

IServiceBehavior の Validate メソッドは、サービス ホスト インスタンスを使用してホストの Closed イベントをサブスクライブし、そこで ThreadPoolHelper にプール内のすべてのスレッドを終了するよう依頼します。

ThreadPoolHelper は、そのサービスの種類の全エンドポイントの全ディスパッチャを、ThreadPoolSynchronizer の同じインスタンスに関連付けます。これにより、すべての呼び出しが同じプールにルーティングされるようになります。ThreadPoolHelper は、サービスの種類を特定の ThreadPoolSynchronizer にマップする必要があるため、サービスの種類をキーとして、また ThreadPoolSynchronizer インスタンスを値として使用する m_Synchronizers という静的な辞書を宣言します。

ApplyDispatchBehavior では、指定されたサービスの種類が m_Synchronizers に含まれているかどうかを ThreadPoolHelper がチェックします。この種類が見つからない場合は、指定された ThreadPoolSynchronizer を ThreadPoolHelper が m_Synchronizers に追加し、サービスの種類に関連付けます。その後に、ThreadPoolHelper が ThreadPoolSynchronizer インスタンスをディスパッチャに割り当てます。

dispatch.SynchronizationContext = m_Synchronizers[type];

この 1 行で、WCF ではこれ以後カスタム同期コンテキストが使用されるようになります。CloseThreads メソッドでは、ThreadPoolHelper が ThreadPoolSynchronizer インスタンスを辞書で検索し、これを破棄します (こうしてプール内のすべてのワーカー スレッドを正常に終了します)。辞書へのアクセスはすべて、MethodImpl 属性と MethodImplOptions.Synchronized フラグを通じて自動的に同期されます。ThreadPoolHelper は、指定されたプール サイズ値がディスパッチャ スロットルの同時呼び出し数の最大値を超えていないことも検証します (図 2 には示されていません)。

スレッドの関係の同期コンテキスト

プール サイズ 1 を指定すると、サービス同時実行モードおよびインスタンス モードとは無関係に、ある特定のスレッドとすべてのサービス呼び出しの間に実質的に関係が作成されます。これが特に役立つのは、サービスがポップアップ ウィンドウなどの UI を作成し、その表示、非表示、更新を定期的に行う必要がある場合です。ウィンドウの作成元であるサービスは、自らが同じスレッド上で呼び出されるようにする必要があります。スレッドの関係のもう 1 つの用途は、TLS を使用するリソースをアクセスまたは作成するサービスです。このような要件を形式化するために、次のような特殊な AffinitySynchronizer を作成しました。

public class AffinitySynchronizer : ThreadPoolSynchronizer
{
   public AffinitySynchronizer() :
         this("AffinitySynchronizer Worker Thread")
   {}
   public AffinitySynchronizer(string threadName): base(1,threadName)
   {}
}

AffinitySynchronizer は前に示したコードと同じようにインストールできますが、同時に私は図 3 の ThreadAffinityBehaviorAttribute を定義しました。この属性は次のように使用できます。

Figure 3 ThreadAffinityBehaviorAttribute (Close Figure 3)
[AttributeUsage(AttributeTargets.Class)]
public class ThreadAffinityBehaviorAttribute :
                                       ThreadPoolBehaviorAttribute
{
   public ThreadAffinityBehaviorAttribute(Type serviceType)
          : this(serviceType,"Affinity Worker Thread")
   {}

   public ThreadAffinityBehaviorAttribute(
Type serviceType,string threadName):base(1,serviceType,
                                                   threadName)
   {}
}

[ThreadAffinityBehavior(typeof(MyService))]
class MyService : IMyContract
{...}

スレッドの関係に依存している場合は、単一の (かつ同一の) スレッドのみがすべてのサービス インスタンスにアクセスできるため、これらのインスタンスは常にスレッド セーフです。ConcurrencyMode.Multiple でサービスを構成すると、シングル スレッド アクセスが作成されます。また、ConcurrencyMode.Reentrant で構成すると、実際には再入が行われないことに注意してください。これは、シングル スレッドが呼び出しでブロックされている間は再入が不可能であるためです。スレッドの関係に依存する場合は、既定値の ConcurrencyMode.Single を選択するのが最も適切です。

優先順位の処理

既定では、WCF サービスのすべての呼び出しが到着順に処理されます。これは、I/O 完了ポート スレッド プールとカスタム スレッド プールのどちらを使用する場合も同じです。通常はこれが望ましい動作です。ところが、一部の呼び出しの優先順位が高く、それらを順番に処理するのではなく到着と同時に処理したい場合はどうなるでしょうか。それどころか、サービスにかかる負荷で基になるサービス リソースが使い果たされたらどうなるでしょうか。また、スロットルが限界に達したらどうなるでしょうか。このような場合、より優先度の高い呼び出しも他の呼び出しと同じようにキューに登録され、サービスまたはそのリソースが利用可能になるのを待ちます。同期コンテキストは、この問題を見事に解決します。まず呼び出しに優先順位が割り当てられ、同期コンテキストで呼び出しが到着順に並べ替えられた後に、それらの呼び出しが実行のためにスレッド プールにディスパッチされます。これは、私が作成した PrioritySynchronizer クラス (図 4 で定義) で行われる処理とまったく同じです。

Figure 4 PrioritySynchronizer (Close Figure 4)
public enum CallPriority
{
   Low,
   Normal,
   High
}
public class PrioritySynchronizer : ThreadPoolSynchronizer
{
   public PrioritySynchronizer(uint poolSize);
   public PrioritySynchronizer(uint poolSize,string poolName);

   public static CallPriority Priority
   {get;set;}
}

PrioritySynchronizer は ThreadPoolSynchronizer から派生したもので、先ほど説明した並べ替えを追加します。SynchronizationContext の Send および Post メソッドは優先順位パラメータを受け取らないため、PrioritySynchronizer のクライアントは 2 つの方法で呼び出しの優先順位を渡します。1 つは Priority プロパティを使用する方法です。この方法では、優先順位 (enum 型の CallPriority の値) が呼び出し元スレッドの TLS に格納されます。もう 1 つはメッセージ ヘッダーを使用する方法です。これについては、このコラムで後ほど説明します。Priority が指定されなかった場合は、既定で CallPriority.Normal に設定されます。

また、対応する PriorityCallsBehaviorAttribute も提供しています (図 5)。PriorityCallsBehaviorAttribute は、次のように簡単に使用できます。

Figure 5 PriorityCallsBehaviorAttribute の実装 (Close Figure 5)
[AttributeUsage(AttributeTargets.Class)]
public class PriorityCallsBehaviorAttribute : ThreadPoolBehaviorAttribute
{
   public PriorityCallsBehaviorAttribute(uint poolSize,Type serviceType)
            : this(poolSize,serviceType,null)
   {}
   public PriorityCallsBehaviorAttribute(uint poolSize,Type serviceType,
       string poolName) : base(poolSize,serviceType,poolName)
   {}
   protected override ThreadPoolSynchronizer ProvideSynchronizer()
   {
      if(ThreadPoolHelper.HasSynchronizer(ServiceType) == false)
         return new PrioritySynchronizer(PoolSize,PoolName);
      else
         return ThreadPoolHelper.GetSynchronizer(ServiceType);
   }
}

[PriorityCallsBehavior(3,typeof(MyService))]
class MyService : IMyContract
{...}

PriorityCallsBehaviorAttribute は、ProvideSynchronizer をオーバーライドし、ThreadPoolSynchronizer の代わりに PrioritySynchronizer を提供します。PrioritySynchronizer は ThreadPoolSynchronizer から派生しているため、ThreadPoolHelper に関する限り透過的です。

優先順位の処理を実装しサポートする際の本当の課題は、クライアントからサービス、そして最終的に PrioritySynchronizer へと呼び出しの優先順位を渡すことです。PrioritySynchronizer の Priority プロパティは、同期コンテキストと直接対話する WCF 以外のクライアントにのみ役立ち、サービスへのアクセスに自身のスレッドが使用されない WCF クライアントには役に立ちません。優先順位は、すべてのメソッドで明示的なパラメータとして指定できますが、どのコントラクトとサービスにも適用できる汎用的なメカニズムが必要であると考えました。そのためには、呼び出しの優先順位を帯域外で、つまりメッセージ ヘッダーを通じて渡す必要があります。2007 年 2 月のコラム「基礎 : キューに登録する WCF 応答サービスを構築する」 (msdn.microsoft.com/msdnmag/issues/07/02/Foundations) では、応答サービスのアドレスを帯域外でサービスに渡すという同様の問題を取り上げ、着信/発信ヘッダーの使用について詳しく説明しました。まったく同じテクニックをここでも使用できますが、それでは同じ説明を繰り返すことになります。そこで今回は、前のコラムで示したテクニックを拡張し、クライアントからサービスに渡す付加情報の汎用的な管理、つまり汎用的ですがタイプ セーフでアプリケーション固有のカスタム コンテキストで、WCF を強化します。これは、GenericContext<T> クラスから提供されます。図 6 は、エラー処理を除いたその実装を示しています。

Figure 6 GenericContext<T> クラス (Close Figure 6)
[DataContract]
public class GenericContext<T>
{
   [DataMember]
   public readonly T Value;

   public GenericContext(T value)
   {
      Value = value;
   }
   public GenericContext() : this(default(T))
   {}
   public static GenericContext<T> Current
   {
      get
      {
         OperationContext context = OperationContext.Current;
         if(context == null)
            return null;

      return context.IncomingMessageHeaders.GetHeader<GenericContext<T>>
         typeof(T).ToString(), typeof(T).Namespace);
      }
      set
      {
         OperationContext context = OperationContext.Current;
         MessageHeader<GenericContext<T>> genericHeader =
            new MessageHeader<GenericContext<T>>(value);
         context.OutgoingMessageHeaders.Add(genericHeader.GetUntypedHeader(
            typeof(T).ToString(), typeof(T).Namespace));
      }
   }
}

GenericContext<T> は、メッセージ ヘッダーとの対話を完全にカプセル化します。サービスに追加情報を渡すためにクライアントに必要な作業は、新しい OperationContextScope 内で静的な Current プロパティを設定することだけです。たとえば、ヘッダーで数値 4 を渡す場合は、図 7 に示すコードを使用します。GenericContext<T> は、図 8 に示す IntContext などの型固有の派生を通じてさらにカプセル化できます。

Figure 8 整数コンテキスト (Close Figure 8)
[DataContract]
public class IntContext : GenericContext<int>
{
   public IntContext()
   {}

   public IntContext(int number) : base(number)
   {}

   public static int Number
   {
      get
         return Current.Value;
      set
         Current = new IntContext(value);
   }
}
Figure 7 GenericContext<T> の使用 (Close Figure 7)
MyContractClient proxy = new MyContractClient();

using(OperationContextScope contextScope =
    new OperationContextScope(proxy.InnerChannel))
{
   GenericContext<int>.Current = new GenericContext<int>(4);
   proxy.MyMethod();
}

IntContext を使用すると、図 7 に次のような行が含まれることになります。

IntContext.Current = 4;

カスタム コンテキストの型パラメータには、事実上あらゆるデータ コントラクト (またはシリアル化可能な) 型を使用できます。サービス側では、カスタム ヘッダーから値を読み取るために、下流の当事者が次のコードを記述します。

int number = GenericContext<int>.Current.Value;

または、型固有のカスタム コンテキストを使用します。

int number = IntContext.Number;

これは、PrioritySynchronizer が呼び出しの優先順位を調べる際に行う処理です。PrioritySynchronizer は、図 7 に似た次のコードを使用して、クライアントが TLS (Priority プロパティ経由) で優先順位を提供するか、メッセージ ヘッダーに優先順位を格納するカスタム コンテキストの形式で優先順位を提供することを予期します。

GenericContext<CallPriority>.Current =
        new GenericContext<CallPriority>(CallPriority.High);
proxy.MyMethod();

しかしこのようなコードには不満がありました。理由は、コードが公開されていること、クライアントを優先順位メカニズムに結合すること、そしてプロキシを使用するたびに事前に優先順位を設定するようクライアントに強要することです。これらの問題に対処するために作成したのが、図 9 の HeaderClientBase<T,H> プロキシ基本クラスです。

Figure 9 HeaderClientBase<T,H> (Close Figure 9)
public abstract partial class HeaderClientBase<T,H> :
ClientBase<T> where T : class
{
   protected H Header;

   public HeaderClientBase() : this(default(H))
   {}

   public HeaderClientBase(string endpointName)
                                   : this(default(H),endpointName)
   {}

   public HeaderClientBase(H header)
   {
      Header = header;
   }

   public HeaderClientBase(H header,string endpointName)
                                    : base(endpointName)
   {
      Header = header;
   }

   /* More constructors */

   protected virtual object Invoke(string operation,params object[] args)
   {
      using(OperationContextScope contextScope =
           new OperationContextScope(InnerChannel))
      {
         GenericContext<H>.Current = new GenericContext<H>(Header);

         Type contract = typeof(T);
         MethodInfo methodInfo = contract.GetMethod(operation);
         return methodInfo.Invoke(Channel,args);
     }
   }
}

HeaderClientBase<T,H> は、その基本クラスと同様にコントラクト型パラメータ T を受け取りますが、同時にカスタム ヘッダー型 H も受け取ります。HeaderClientBase<T,H> は、H の既定値または構築時に指定された値を使用して、すべての呼び出しに渡します。SvcUtil のようなコード ジェネレータ サポートがなかったので、汎用コンテキストおよび操作コンテキスト スコープとの対話を完全にカプセル化する一方で、リフレクションを使用して実際のサービス操作を呼び出すようにしました。ユーザーは、HeaderClientBase<T,H> からクラスを派生し、図 10 のように必要なヘッダー型を指定して整数を提供する必要があります。

Figure 10 HeaderClientBase<T,H> の使用 (Close Figure 10)
[ServiceContract]
interface IMyContract
{
   [OperationContract]
   void MyMethod(string text);
}

class MyContractClient : HeaderClientBase<IMyContract,int>,IMyContract
{
   public MyContractClient(int number) : base(number)
   {}
   public void MyMethod(string text)
   {
      Invoke("MyMethod",text);
   }
}

図 10 のコードを使用することで、図 7 のコードは次のように短くなります。

MyContractClient proxy = new MyContractClient(4);
proxy.MyMethod();

図 10 と同じテクニックを使って呼び出しの優先順位を渡すこともできますが、汎用性を考えると、汎用の優先順位対応プロキシ PriorityClientBase<T> (図 11) を定義した方がいいでしょう。

Figure 11 PriorityClientBase<T> の定義 (Close Figure 11)
public abstract partial class PriorityClientBase<T> :
     HeaderClientBase<T,CallPriority> where T : class
{
   public PriorityClientBase() : this(PrioritySynchronizer.Priority)
   {}

   public PriorityClientBase(string endpointName) :
       this(PrioritySynchronizer.Priority,endpointName)
   {}

   public PriorityClientBase(Binding binding,
EndpointAddress remoteAddress) :
       this(PrioritySynchronizer.Priority,binding,remoteAddress)
   {}

   public PriorityClientBase(CallPriority priority) : base(priority)
   {}

   public PriorityClientBase(CallPriority priority,string endpointName) :
       base(priority,endpointName)
   {}

   public PriorityClientBase(CallPriority priority,Binding binding,
       EndpointAddress remoteAddress) :
       base(priority,binding,remoteAddress)
   {}
   /* More constructors */
}

PriorityClientBase<T> は、型パラメータ H に CallPriority を使用するようにハードコードされています。また PriorityClientBase<T> は、既定で TLS から優先順位を読み取るので (見つからない場合は CallPriority.Normal を使用)、他のプロキシ クラスと同じように使用できます。図 12 に示すように、既存のプロキシ クラスにわずかな変更を加えるだけで、優先順位処理がサポートされるようになります。

Figure 12 優先順位処理のサポート (Close Figure 12)
class MyContractClient : PriorityClientBase<IMyContract>,IMyContract
{
   //Reads priority from TLS
   public MyContractClient()
   {}

   public MyContractClient(CallPriority priority) : base(priority)
   {}
   public void MyMethod(string text)
   {
      Invoke("MyMethod",text);
   }
}

MyContractClient proxy = new MyContractClient(CallPriority.High);
proxy.MyMethod();

コールバックと同期コンテキスト

WCF では、callback 型に CallbackBehaviorAttribute が適用されたコールバック オブジェクトにも、同期コンテキストに関する同様のサポートが提供されます。

[AttributeUsage(AttributeTargets.Class)]
public sealed class CallbackBehaviorAttribute : Attribute,...
{
   public bool UseSynchronizationContext
   {get;set;}
   //More members
}

ServiceBehaviorAttribute と同様に、UseSynchronizationContext は既定で true に設定されます。コールバック オブジェクトの興味深い局面は、同期コンテキストとの関係が確定されるときです。WCF は、二重通信を確立するプロキシを開いたスレッドを調べます。このスレッドに同期コンテキストがある場合は、そのコンテキストがすべてのコールバックに使用されます。カスタム同期コンテキストは、プロキシを開く前に明示的に設定する方法で手動でインストールできますが、この処理を属性を使用して宣言的に行うことをお勧めします。コールバック エンドポイント ディスパッチャに作用させるには、この属性で IEndpointBehavior インターフェイスを実装する必要があります。

public interface IEndpointBehavior
{
   void ApplyClientBehavior(ServiceEndpoint endpoint,
                            ClientRuntime clientRuntime);
   //More members
}

ApplyClientBehavior メソッドでは、ClientRuntime パラメータに、CallbackDispatchRuntime プロパティを持つエンドポイント ディスパッチャへの参照が格納されます。

public sealed class ClientRuntime
{
   public DispatchRuntime CallbackDispatchRuntime
   {get;}
   //More members
}

残りは、図 13 の CallbackThreadPoolBehaviorAttribute で示すように、サービス側の属性とまったく同じです。

Figure 13 CallbackThreadPoolBehaviorAttribute (Close Figure 13)
[AttributeUsage(AttributeTargets.Class)]
public class CallbackThreadPoolBehaviorAttribute :
 ThreadPoolBehaviorAttribute,IEndpointBehavior
{
   public CallbackThreadPoolBehaviorAttribute(uint poolSize,
Type clientType) : this(poolSize,clientType,null)
   {}
   public CallbackThreadPoolBehaviorAttribute(uint poolSize,
Type clientType,string poolName) :
 base(poolSize,clientType,poolName)
   {
      AppDomain.CurrentDomain.ProcessExit += delegate
      {
         ThreadPoolHelper.CloseThreads(ServiceType);
      };
   }
   void IEndpointBehavior.ApplyClientBehavior
      (ServiceEndpoint serviceEndpoint,ClientRuntime clientRuntime)
   {
      IContractBehavior contractBehavior = this;
      contractBehavior.ApplyDispatchBehavior(null,serviceEndpoint,
         clientRuntime.CallbackDispatchRuntime);
   }
   //Rest of the implementation
}

コールバック属性では、サービス属性をできる限り再利用したいと考えました。そのために、CallbackThreadPoolBehaviorAttribute は ThreadPoolBehaviorAttribute から派生します。そのコンストラクタは、基本コンストラクタにクライアントの種類をサービスの種類として渡します。ApplyClientBehavior の CallbackThreadPoolBehaviorAttribute 実装は、その基本クラスの IContractBehavior を照会し (この方法で、サブクラスはその基本クラスの明示的なプライベート インターフェイス実装を使用します)、その実装を ApplyDispatchBehavior にデリゲートします。

クライアント コールバック属性とサービス属性の大きな違いは、コールバックのシナリオに Closed イベントをサブスクライブするホスト オブジェクトがないことです。これを補うために、CallbackThreadPoolBehaviorAttribute はプロセス終了イベントを監視して、プール内のすべてのスレッドを終了します。クライアントは、図 14 の ThreadPoolBehavior.CloseThreads を使用することで、これらのスレッドを迅速に終了できます。

Figure 14 CallbackThreadPoolBehaviorAttribute の使用 (Close Figure 14)
[ServiceContract(CallbackContract = typeof(IMyContractCallback))]
interface IMyContract
{
   [OperationContract]
   void MyMethod();
}
interface IMyContractCallback
{
   [OperationContract]
   void OnCallback();
}

[CallbackThreadPoolBehavior(3,typeof(MyClient))]
class MyClient : IMyContractCallback,IDisposable
{
   MyContractClient m_Proxy;

   public void CallService()
   {
      m_Proxy = new MyContractClient(new InstanceContext(this));

      m_Proxy.MyMethod();
   }

   //Called by threads from the custom pool
   public void OnCallback()
   {...}

   public void Dispose()
   {
      m_Proxy.Close();
      ThreadPoolHelper.CloseThreads(typeof(MyClient));
   }
}

すべてのコールバックを同じスレッドで実行するには (コールバック側で何らかの UI を作成する場合など)、サービス側と同様に、プール サイズが 1 になるようにコールバックを構成します。さらに良い方法として、図 15 のように、このような同一スレッド関係を実現する専用のコールバック属性を定義することもできます。

Figure 15 CallbackThreadAffinityBehaviorAttribute (Close Figure 15)
[AttributeUsage(AttributeTargets.Class)]
public class CallbackThreadAffinityBehaviorAttribute :
                     CallbackThreadPoolBehaviorAttribute
{
   public CallbackThreadAffinityBehaviorAttribute(Type clientType) :
        this(clientType,"Callback Worker Thread")
   {}
   public CallbackThreadAffinityBehaviorAttribute(Type clientType,
        string threadName) : base(1,clientType,threadName)
   {}
}

同期コンテキストを使用する理由

.NET Framework 2.0 の同期コンテキストは強力な構成概念であり、WCF と組み合わせることで、生産性の高い優れたプログラミング モデルをもたらします。この組み合わせを特に強力なものにするのが WCF の拡張性です。この拡張性によって、スレッドの関係や優先順位処理などの困難な作業を、比較的少ないコードで、また完全にサービス スコープの外部で実現できます。このコラムで紹介したカスタム ヘッダーおよび汎用コンテキストのテクニックは、WCF プログラミングの領域全体で利用できます。帯域外パラメータは、どこで呼び出されても、的確で簡潔なタイプ セーフの解決策となります。この記事は、私の思考プロセス、そして WCF の拡張機能を独自に開発する際に考慮すべきトレードオフを、読者のみなさんと共有することも目的としています。

ご意見やご質問は、

こちら mmnet30@microsoft.com.

まで英語でお送りください。
Juval Lowy は、WCF トレーニングと WCF アーキテクチャ コンサルティングを行う IDesign 社に所属するソフトウェア アーキテクトです。最近の著作としては、『WCF サービスのプログラミング』 (O'Reilly、2007 年) があります。また、シリコン バレー地域の Microsoft Regional Director も務めています。連絡先は、www.idesign.net です。


Bugslayer: ビュー ステートの影響を測定する

Bugslayer
ビュー ステートの影響を測定する
John Robbins

コードのダウンロード : : Bugslayer2007_11.exe (209KB)
コンテンツ
ビュー ステート
デバッグのヒント
目標
ビュー ステートをトレースする
実装の要点
その他のアイデア

Microsoft® .NET Framework がメモリについて考慮する必要がない環境だと考えられているのはおかしなことだと思いませんか。今日常にマネージ アプリケーションでネックになっている大きな問題の 1 つは何でしょうか。それはメモリです。なぜでしょう。メモリを再利用するためにガベージ コレクタが実行されると、共通言語ランタイム (CLR) によってアプリケーションのすべてのスレッドが中断され、どの作業も実行されなくなります。どの作業も実行されなくなると、パフォーマンスの問題が生じます。

.NET Framework のメモリ使用量についてどれだけ注意しても、大きな問題がいくつかあるので、実際にそのメモリの状況を把握するのは困難です。1 つ目の問題は、.NET Framework では膨大な作業が自動的に実行されますが、実行されている作業や使用されているメモリ量を把握するために必ずしもドキュメントが役に立つとは限らないことです。さいわいなことに、Lutz Roeder の Reflector for .NET (aisto.com/roeder/dotnet) が非常に役立ちます。

メモリ使用量の監視に関する 2 つ目の問題は、.NET の主要な開発ツールである Visual Studio® に、メモリに関する情報が何も表示されないことです。これは Visual Studio チームの責任ではなく、元の CLR アーキテクチャの欠点です。このアーキテクチャでは、CLR のデバッグ API を使用して特定のオブジェクトが属している世代を確認する手段が公開されていません。CLR のプロファイル API を使用してオブジェクトの世代を追跡することはできますが、非常に手間がかかります。また、デバッグ中にはこの処理は機能しません。2003 年 6 月 (msdn.microsoft.com/msdnmag/issues/03/06/Bugslayer) と 2005 年 3 月 (msdn.microsoft.com/msdnmag/issues/05/03/Bugslayer) の Bugslayer コラムで説明したように、Son of Strike (SOS) WinDBG 拡張機能を使用すると必要なすべてのメモリ情報を確認できますが、これは、元の Windows® 95 カーネル デバッガ以降使用されているツールの中でおそらく最も手間がかかるツールです。

今回の Bugslayer コラムでは、ASP.NET アプリケーションで最も発見されにくいパフォーマンス低下の要因の 1 つであるビュー ステートを確認できるツールを紹介します。後ほど説明するように、運用サーバーでビュー ステート使用量を簡単に確認できれば 30 分のデバッグで済むようになり、6 週間かけて推測する必要はなくなります。

ビュー ステート

ASP.NET ページのビュー ステートは非常に高度なメカニズムで、ページ内のコントロールの状態が、クライアントに送信される HTML コード内の非表示の値に格納されます。これにより、ページの状態が ASP.NET ワーカー プロセスに保持されなくなり、サーバーのメモリがさらに不足するようになります。インターネット黎明期 (1996 年頃) にネイティブ C++ で記述する ISAPI フィルタに取り組んだことがある人は、ページ、セッション、およびアプリケーションの状態の追跡にどれだけ手間がかかったかを思い出してください。ASP.NET の使用経験があまりない場合は、入門書として Dino Esposito の 2003 年 2 月のビュー ステートに関する Cutting Edge コラム (msdn.microsoft.com/msdnmag/issues/03/02/cuttingedge) を参照してください。

ビュー ステートには、ある時点におけるそのビュー ステートの値の大きさがわからないという問題があります。これは、ページに GridView コントロールがある場合に特に当てはまります。ページでそのデータベース情報を取得する際に使いやすくするためにはコストがかかります。さいわい、ASP.NET 2.0 では、マイクロソフトの開発者たちの尽力により、ビュー ステートのサイズが ASP.NET 1.1 と比較して小さくなっています。しかし、私はこの数か月に、いくつかの大規模な ASP.NET 2.0 アプリケーションのデバッグに協力しました。これらのアプリケーションでは、特定のページのビュー ステートのサイズが肥大化したためにパフォーマンスの問題が発生していました。

デバッグのヒント

ツールとコードについて説明する前に、デバッグの秘訣をいくつか説明します。パフォーマンスに問題がある ASP.NET アプリケーションについて開発者から連絡を受けたとき、必ず耳にするのは「アプリケーションは正常に動作していたが、今は動作がかなり遅くなっている」という内容です。

私はまず、「アプリケーションで GridView を使用していますか」と質問します。ほとんどの ASP.NET アプリケーションがデータベースのフロントエンドなので、たいていは GridView を使用しています。ただし、例外もあります。GridView を使用していない場合は、別の方法を考える必要があります。

次に、「バージョン管理でデータベースのストアド プロシージャ履歴を拝見できますか」と質問します。残念ながら、ここであまりに多くの開発チームが言葉に詰まり、バージョン管理にストアド プロシージャを保存していないと答えるのです。前回確認したとき、SQL ストアド プロシージャはコードでした。開発の常識的なルールに従えば、すべてのコードはバージョン管理に保存されているはずです。実際、この処理は Microsoft Visual Studio 2005 Team Edition for Database Professionals を使用すると非常に簡単に実行できます。バージョン管理にストアド プロシージャを保存していない場合は、ここで今すぐストアド プロシージャを追加する必要があります。

バージョン管理でストアド プロシージャを確認できるかどうかを質問する理由は、アプリケーションが正常に実行されていたのに突然問題が生じ始めた場合は、前のエディションと現在のエディションの間の変更を見つける必要があるためです。これまでに何度バージョン管理のストアド プロシージャに関する変更ログを調べてきたかわかりません。その中で、クエリが次のように変更されているのを何度も見てきました。

   select * from table

その出力を (ビュー ステートに大きく依存している) GridView コントロールに送っており、データベースに 600 万件のレコードが格納されている場合、ビュー ステートに関する深刻な問題が生じます。

ビュー ステートは使いやすい機能ですが、いつ問題が生じたかを確認するのはほぼ不可能です。ほとんどの開発者は、ASP.NET トレースをオンにするとページのビュー ステートを確認できることを知っています。残念ながら、ASP.NET トレースでは、格納されるトレースの数が限られています。また、運用サーバーでの動作が非常に遅くなる場合があります。他には、各ユーザーがブラウザで右クリックし、コンテキスト メニューから [ソースの表示] を選択して、文字のカウントを手動で開始する方法しかありませんでした。明らかに、もっと良い方法が必要でした。つまり、データを 1 か所にまとめてデバッグに関する適切な決定を行うことができるように、運用サーバー上のすべてのページの最大ビュー ステートを追跡する簡単な方法が必要でした。

目標

ビュー ステート使用量を簡単に確認できるようにするツールの課題について最初に考え始めたとき、最も重要な目標は、とにかく運用環境で使用できるものでなければならないということでした。つまり、高速でメモリ消費が少ないコードが必要でした。そのため、各ページの最大ビュー ステートのサイズのみを格納することに決めました。各ページのすべてのビュー ステートのサイズを保持すると、すぐに何百万件ものレコードを格納する羽目になってしまいます。特定のページの最大ビュー ステートのみを格納することによって、必要となる記憶域を大幅に削減し、データ エントリ数が ASP.NET アプリケーション内の .aspx ファイル数を超えないようにしました。

また、ビュー ステート データの確認を、開発者以外の人にとっても簡単でわかりやすくしようと考えました。ページのビュー ステートの合計サイズの確認も有効ですが、ページ内のすべてのコントロールのビュー ステートのサイズを格納して表示するオプションも要件として追加しました。これにより、問題の最大の要因となっているコントロールを簡単に特定できるようになります。

HTTP GET 要求のビュー ステートのサイズを確認するオプションも追加しようと考えました。ほぼすべてのビュー ステートの問題は HTTP POST 要求に関連していますが、ユーザーが完全な URL を特定のページに格納し、そのページに直接アクセスする場合があります。これはめったにないことですが、完全を期するためにこのオプションを含めようと考えました。

最後に、ツールを簡単に展開できるようにしようと考えました。努力目標として、このソリューションでソース コードの変更が不要であれば理想的でした。

今月のコラムには、これらすべての機能を含むアセンブリ Bugslayer.Web が付属しており、ソース コードのダウンロードが可能です。完全な単体テストも付属しており、アセンブリを使用してその動作を確認できるようになっています。運用サーバーの場合は、Bugslayer.Web.DLL をグローバル アセンブリ キャッシュ (GAC) にインストールできます。これにより、コンピュータ上のどこからでもコードを読み込むことができるようになります。

Bugslayer.Web には HttpModule と HttpHandler の 2 つの部分があります。ASP.NET 処理パイプラインについて熟知している人は、おそらく HttpModule でデータ収集が行われ、HttpHandler にデータが表示されると想像できるでしょう。つまり、Bugslayer.Web を使用するためにコードを変更する必要はありません。

HttpModule を使用するように ASP.NET アプリケーションを構成するには、次のように <httpModules> 要素を web.config ファイルに追加する必要があります。

<?xml version="1.0"?>
<configuration>
  <system.web>
    <httpModules>
      <add name="ViewStateStats"
          type="Bugslayer.Web.ViewStateStatisticsModule"/>
    </httpModules>
  </system.web>
</configuration>

HttpHandler の設定も簡単です。次のように、<httpHandlers> 要素を web.config ファイルに追加します。

<?xml version="1.0"?>
<configuration>
  <system.web>
    <httpHandlers>
      <add verb="*"
           path="viewstatestats.axd"
           type="Bugslayer.Web.ViewStateStatisticsHandler"
           validate="false" />
    </httpHandlers>
  </system.web>
</configuration>

Bugslayer.Web.ViewStateStatisticsHandler が viewstatestats.axd にマップされています。.axd 拡張子は IIS で ASP.NET 拡張子の種類として既に認識されているので、この拡張子を使用することにしました。path 属性を特定のファイル名に設定すると、ASP.NET によってその特定の HttpHandler が呼び出されますが、その他のファイル名の要求は ASP.NET の内部 .axd ハンドラにルーティングされます。

ビュー ステートをトレースする

Bugslayer.Web をインストールしてエントリを web.config に追加したら、各ページの最大ビュー ステートのバイト数を記録できるようになります。データの収集も重要ですが、本当に必要なのはそのデータを表示できるようにすることで、既に説明したようにこれは ViewStateStats.axd によって実現されます。図 1 に出力例を示します。ASP.NET トレース出力からスタイルを借用したので、見慣れた形式だと感じるかもしれません。ご覧のとおり、ビュー ステートは既定では最大のものから順に示されます。URL または時刻を基準に並べ替えるには (どちらも昇順で並べ替えられます)、列ヘッダーをクリックします。

図 1 ViewStateStats.axd の出力例
図 1 ViewStateStats.axd の出力例(画像を縮小するには、ここをクリックします)
図 1 ViewStateStats.axd の出力例
図 1 ViewStateStats.axd の出力例 (画像を拡大するには、ここをクリックします)

ViewStateStats.axd ページの上部で、ビュー ステートのデータ収集を制御できます。最もおもしろいオプションは、2 番目のリンクの [Collect Control View State Sizes] (コントロールのビュー ステートのサイズを収集する) です。この状態が無効になっている場合 (既定のオプション)、ページの合計サイズのみが収集されます。リンクをクリックすると収集の状態が切り替わり、各ページの最大ビュー ステートの合計に対する子コントロールとそれぞれのビュー ステートの一覧が保持されるようになります。これによりパフォーマンスへの影響が生じ、メモリ使用量が増加しますが、ビュー ステートを占有しているコントロールの特定が何度も必要になることがあります。図 2 に、コントロールが展開されている ViewStateStats.axd ページの例を示します。

図 2 ViewStateStats.axd のコントロール ツリーの出力
図 2 ViewStateStats.axd のコントロール ツリーの出力(画像を縮小するには、ここをクリックします)
図 2 ViewStateStats.axd のコントロール ツリーの出力
図 2 ViewStateStats.axd のコントロール ツリーの出力 (画像を拡大するには、ここをクリックします)

ASP.NET トレース出力と同様に、コントロール ツリーがコントロール名およびそのビュー ステートのサイズと共に表示されます。レンダリング サイズのバイト値と ControlState サイズのバイト値はどこに表示されているのだろうと思われるかもしれませんが、レンダリング データを取得する方法はありませんでした。また、ControlState サイズの計算にかかる時間は非常に長くなるだろうと感じました。その理由については、「実装の要点」で説明します。ビュー ステートはパフォーマンス低下の主な要因なので、本当に重要なコントロールの影響がそこに示されます。なお、個々のコントロールのサイズを合計しても、URL と時刻の横に示されているビュー ステートの合計サイズにはなりません。ビュー ステートは Base64 でエンコードされているので、エンコードによってビュー ステート全体にバイト数が追加されています。

ViewStateStats.axd タイトルの下の最初のリンク [Clear all view state statistics] (すべてのビュー ステートの統計をクリアする) をクリックすると、保存された既存のすべての値がフラッシュされ、新たに開始されます。最後のリンク [Collect View State Sizes for HTTP GET Requests] (HTTP GET 要求のビュー ステートのサイズを収集する) をクリックすると、HTTP GET 要求に関するレポートがオンになります。ビュー ステートに関する問題はほぼすべて HTTP POST に関連するので、このオプションをオンにする必要はほとんどありません。ただし、HTTP GET と HTTP POST の両方のビュー ステートのサイズを確認する必要がある場合は、このオプションを使用できます。このオプションをオンにすると、ViewStateStats.axd ページの各 URL の後に、(GET) または (POST) が適宜表示されます。HTTP GET のビュー ステートのサイズを計算する方法について説明すれば、このデータを記録する場合にパフォーマンスへの影響が生じることがわかるでしょう。

実装の要点

アプリケーション全体のビュー ステートのサイズを記録するアイデアを最初に練り始めたとき、私は System.Web.UI.Page 派生クラスをプロトタイプとして作成しました。これは、次の C# コードでビュー ステート文字列を取得する簡単なものです。

String viewState = Request.Params [ "__VIEWSTATE" ];

Page 派生クラスを使用する場合の問題は、多くの既存の ASP.NET アプリケーションでは、アプリケーション全体のビュー ステートを確認するためにコードを変更する必要があることです。HttpModule はコードを通常の ASP.NET パイプラインに参加させることができるので、解決策として完璧でした。ユーザーに送信できる状態になったページだけを調べようと考えたので、IHttpModule.Init 実装メソッドに EndRequest イベント ハンドラを設定しました。

ASP.NET ビュー ステートの動作に関するドキュメントを読んでいたときに、次のような興味深い web.config 設定を見つけました。

<pages maxPageStateFieldLength="XX"/>

ここで、XX は、__VIEWSTATE 値をチャンクに分割する場合の最大バイト数です。これは、System.Web.Ui.Page.MaxPageStateFieldLength プロパティでも設定できます。

ファイアウォールやプロキシの中にはページ内の大量の文字列で過負荷状態になるものもあるので、非常に大きな __VIEWSTATE 値を分割するためにこの設定が ASP.NET 2.0 で導入されました。大量の文字列は避ける必要があるので (これがこのコラム全体のポイントです)、maxPageStateFieldLength を設定するとサイズの縮小に役立つと思われるかもしれません。実際は、役立たないだけでなく、ブラウザに送信されるページが膨れ上がってしまいます。

たとえば、ページに次のようなビュー ステートがあるとします。

<div>
<input type="hidden" name="__VIEWSTATE" id="__VIEWSTATE"
value="/wEPDwULLTE1ODEyMjM1MTgPZBYCAgMPZBYCAgcPEGQPFgNmAgECAhYDEAUBYQUBYWcQBQFiBQFiZxAFAW
MFAWNnZGRkQIU7yEIZAT5uuif4cZcFHBJgZ48=" />
</div>

maxPageStateFieldLength を 50 に設定すると次のようになり、ページ内の実際のテキストは、はるかに増えてしまいます。

<div>
<input type="hidden" name="__VIEWSTATEFIELDCOUNT"
       id="__VIEWSTATEFIELDCOUNT" value="3" />
<input type="hidden" name="__VIEWSTATE"
       id="__VIEWSTATE" value="/wEPDwULLTE1ODEyMjM1MTgPZBYCAgMPZBYCAgcPEGQPFgNmAg" />
<input type="hidden" name="__VIEWSTATE1"
       id="__VIEWSTATE1" value="ECAhYDEAUBYQUBYWcQBQFiBQFiZxAFAWMFAWNnZGRkQIU7yEIZ" />
<input type="hidden" name="__VIEWSTATE2"
       id="__VIEWSTATE2" value="AT5uuif4cZcFHBJgZ48=" />
</div>

maxPageStateFieldLength を設定している読者もいるとは思いますが、多くの方が使用しているとは思えません。ビュー ステートをチャンクに分割してファイアウォールやプロキシの問題を回避している場合は、ネットワーク経由で大量のデータを送信しないようにするために、ページを再設計するかビュー ステートをサーバーに格納することを検討することを強くお勧めします。

ViewStateStatisticsModule.cs に実装されている HttpModule を使用すると、maxPageStateFieldLength が設定されている場合に適切に対処できます。各 __VIEWSTATE* パラメータを列挙する必要があるので、使用するコードは単一の __VIEWSTATE パラメータにアクセスする場合よりも少し低速です。

データの収集と表示は論理的に連結されているので、これらの処理を IViewStateProcessor インターフェイスにグループ化し、そのすべての機能を HttpModule と HttpHandler から切り離すことにしました。IViewStateProcessor インターフェイスには、HTTP GET と HTTP POST の両方の要求を処理するメソッドや、そのデータを表示するメソッドが用意されています。私は、Bugslayer.Web.dll のコードをほとんど変更せずに追加のデータの収集と表示を簡単に実行できるようにしようと考えました。

IViewStateProcessor から派生したクラスは常にメモリ内に存在する必要があるので、そのクラスを ViewStateStatisticsModule クラス内の静的フィールドに格納しています。ViewStateStatisticsHandler クラスは、単に ViewStateStatisticsModule の静的プロパティにアクセスします。現時点では、このコードは単一の IViewStateProcessor 派生クラスのみをサポートしています。追加のクラスを同時に使用できるようにするサポートを追加すると改良につながるでしょう。

IViewStateProcessor から派生した独自のカスタム クラスを代わりに指定するには、HttpApplication.Init メソッドをオーバーライドする必要があります。このメソッドで、HttpApplication.Modules["ViewStateStats"] を使用して ViewStateStatisticsModule にアクセスします。IViewStateProcessor から派生したカスタム クラスのインスタンスを作成し、ViewStateStatisticsModule.ViewStateProcessor をそのインスタンスに設定します。

IViewStateProcessor から派生した既定のクラス BiggestPageProcessor では、図 1 と図 2 に示したデータを収集して表示するためのすべての作業が実際に行われます。収集データは Dictionary クラスに格納され、高速な参照のためのキーとして URL も共に格納されます。収集コードで興味深かったのは、個々のコントロールのビュー ステートを取得する方法を見つけることでした。

個々のコントロールのビュー ステートのサイズを確認することが一般的であるため、数値の正確性を確保できるようにしようと考えました。つまり、ASP.NET トレース コードでアクションが発生している場所を把握する必要がありました。Reflector で最初に逆コンパイルしたのは、trace.axd に登録されている HttpHandler である System.Web.Handlers.TraceHandler でした。約 15 分間逆コンパイルされたコードを調べて参照先のデータ型を検索したところ、System.Web.Ui.Page.ProcessRequestMain が見つかりました。Page.Context.TracingEnabled が true の場合、ここでトレースが実際に行われます。Page.BuildPageProfileTree をじっくり調べて、ついに Control.BuildProfileTree に辿り着きました。ここで、コントロールとそのデータをトレース出力に示すためのすべてのアクションが発生しています。図 3 に、逆コンパイルされたコードを示します。

Figure 3 逆コンパイルされた Control.BuildProfileTree メソッド (Close Figure 3)
protected void BuildProfileTree(string parentId, bool calcViewState)
{
    int viewStateSize;
    calcViewState = calcViewState && !this.flags[4];
    if (calcViewState)
    {
        viewStateSize = this.EstimateStateSize(this.SaveViewState());
    }
    else
    {
        viewStateSize = 0;
    }
    int controlStateSize = 0;
    if (((this.Page != null) &&
         (this.Page._registeredControlsRequiringControlState != null)) &&
         this.Page._registeredControlsRequiringControlState.Contains(
         this))
    {
        controlStateSize =
            this.EstimateStateSize(this.SaveControlStateInternal());
    }
    this.Page.Trace.AddNewControl(this.UniqueID, parentId,
        base.GetType().FullName,  viewStateSize, controlStateSize);
    if ((this._occasionalFields != null) &&
        (this._occasionalFields.Controls != null))
    {
        int count = this._occasionalFields.Controls.Count;
        for (int i = 0; i < count; i++)
        {
            this._occasionalFields.Controls[i].BuildProfileTree(
                this.UniqueID, calcViewState);
        }
    }
}

ビュー ステートのコードの興味深い部分は、メソッドの最初の 10 行です。コードの最初の部分で私が本当に驚いた箇所は !this.flags[4] 参照です。Reflector でフラグ フィールドがプライベートとして表示されたためです。Reflector の非常に便利なアナライザを使用すると、フラグ フィールドにジャンプでき、[Used By] (使用元) ツリー要素をクリックできました。ビュー ステートに関連するものすべてをスキャンすると、Control.EnableViewState プロパティの getter が一覧に含まれているのが示されました。逆コンパイルされた EnableViewState の getter メソッドにジャンプすると、そのメソッドが実際に !this.flags[4] を返しているのが示されたので、謎は解けました。

EstimateStateSize は単に ObjectStateFormatter を作成し、Serialize メソッドを呼び出し、シリアル化のバイト サイズを返すので、その複製は問題なく済みました。本当に興味深いのはプロテクトされた this.SaveViewState メソッドの呼び出しで、私はこのメソッドを HttpModule から呼び出せるようにしようと考えました。.NET Reflection はすべてにアクセスして呼び出すための奥の手なので、これを少し使用して、ASP.NET トレース システムと同じ数値を取得できるようにしました。図 4 に、個々のコントロールのビュー ステートのサイズを計算するために記述した中核のコードを示します。

Figure 4 コントロールのビュー ステートのサイズを計算する中核のコード (Close Figure 4)
private PageViewState CreatePageViewState (
    String url ,
    DateTime timeStamp ,
    Int32 viewStateLength ,
    String httpRequestType )
{
    PageViewState returnValue = new PageViewState (
        url ,
        timeStamp ,
        viewStateLength ,
        httpRequestType );
    if ( true == collectControlData )
    {
        Page currentPage = HttpContext.Current.Handler as Page;
        if ( null != currentPage )
        {
            CalculateControlSizes ( currentPage ,
                                    0 ,
                                    returnValue.ControlList );
        }
    }
    return ( returnValue );
}

private static void CalculateControlSizes ( Control ctl ,
                                            Int32 indentLevel ,
                                            List<ControlViewState> ctlList )
{
    Int32 viewStateSize = EstimateSize ( ctl );
    ControlViewState state = new ControlViewState ( indentLevel ,
                                                ctl.UniqueID ,
                                                ctl.GetType ( ).FullName ,
                                                viewStateSize );
    // Add it to the list.
    ctlList.Add ( state );
    for ( int i = 0 ; i < ctl.Controls.Count ; i++ )
    {
        CalculateControlSizes ( ctl.Controls [ i ] , indentLevel + 1 ,
                                ctlList );
    }
}

private static Int32 EstimateSize ( Control ctl )
{
    // In Control.BuildProfileTree, the check is done against flag[4],
    // which is the same as the implementation of EnableViewState.
    if ( false == ctl.EnableViewState )
    {
        return ( 0 );
    }
    Object saved = CallSaveViewStateMethod ( ctl );
    if ( null == saved )
    {
        return ( 0 );
    }
    ObjectStateFormatter osf = new ObjectStateFormatter ( );
    Int32 ret = osf.Serialize ( saved ).Length;
    return ( ret );
}

private static Object CallSaveViewStateMethod ( Control ctl )
{
    // This is way ugly, but the SaveViewState method is protected on
    // the Control class so in order to get the total size, I need to
    // call it through Reflection.
    Object ret = null;
    BindingFlags flags = BindingFlags.Instance | BindingFlags.Public |
                            BindingFlags.NonPublic;
    Type type = ctl.GetType ( );
    MethodInfo m = type.GetMethod ( "SaveViewState" , flags );
    if ( null != m )
    {
        ret = m.Invoke ( ctl , null );
    }
    return ( ret );
}

private Int32 SumControlSizes ( Control ctl )
{
    Int32 viewStateSize = EstimateSize ( ctl );
    for ( int i = 0 ; i < ctl.Controls.Count ; i++ )
    {
        viewStateSize += SumControlSizes ( ctl.Controls [ i ] );
    }
    return ( viewStateSize );
}

ASP.NET トレース コードと同様にビュー ステートを計算する秘訣がわかったら、次は HttpModule.EndRequest イベント ハンドラ内のビュー ステートの値を要求したときにその値が同じかどうかを確認するテストを開始する必要がありました。さいわい、Bugslayer.Web を使用してあらゆる種類のページを実行した後、トレースでレポートされた値と私が計算した値の間に相違は見られませんでした。

ちなみに、レンダリング サイズのバイト値を表示に含めなかったのは、この値を計算する方法がなかったためです。これはページのレンダリング中に発生し、パイプラインでの HttpModule の処理があまりに低速になります。パフォーマンス上の理由から、ControlState サイズのバイト値は計算しないことにしました。図 3 に、計算に必要な作業を示します。ただし、これらの呼び出しを行うには低速なリフレクションを多数使用する必要がありますが、見返りはほとんどありません。

鋭い読者は、Request.Params["__VIEWSTATE"] を使用してビュー ステートを取得することは HTTP GET 要求に存在しないフォームの変数にアクセスすることであると私が説明していることにお気付きかもしれません。しかし、ブラウザでページのソース コードを確認すると、__VIEWSTATE 変数は確かに存在しているので、ユーザーがブラウザのアドレス バーから直接ページにアクセスする場合のデータを取得する方法を見つけようと私は考えました。

あらゆる調査を重ねた結果、コントロールをループ処理して各ビュー ステートのサイズを取得することによって自分で計算を行うことが最善の解決策であると考えました。理想的ではありませんが、完全な BASE64 エンコードとオーバーヘッドの増加を避けて実際のビュー ステートのサイズに近づくことができます。HTTP GET 処理に関するこの制限事項のため、私はその監視オプションが既定でオフになるようにしました。

その他のアイデア

概して、ビュー ステートの監視ツールのできに私は大変満足しています。このツールを既存の運用アプリケーションに簡単に統合できるようにすると、肥大化したビュー ステートの検出という目標をより簡単に達成できるようになります。ビュー ステートにパフォーマンス低下の要因が潜んでいる場合、ページのビュー ステートのサイズを簡単に確認できるとデバッグにかかる時間を短縮できるので、新しい機能や私たちがこのビジネスに携わっている理由となっている楽しい作業に取り組めるようになります。

すべての Bugslayer コラムと同様に、ビュー ステートの状況に関する追加情報を追跡するために Bugslayer.Web に追加できる機能は常に提供されます。

このコラムで入手できるバージョンのツールでは、__VIEWSTATE 非表示フィールド形式のページのビュー ステートのみが追跡されます。これが最も大きなパフォーマンス低下の要因になるためです。サーバーに格納されているビュー ステートの情報を追跡するように Bugslayer.Web を拡張することも検討できます。これには追加の作業が必要です。Page から派生したカスタム クラスを指定し、Page.SaveViewState メソッドをオーバーライドする必要があります。

サンプルでは、ビュー ステートは HttpApplication 単位でのみ保存されます。Web ファーム全体で最大のビュー ステートを追跡できるようにすると改良につながるでしょう。

コントロール ツリーを ASP.NET トレースで表示されるツリーと同様にしようと考えたので、ビュー ステート データがないコントロールも含めています。ビュー ステートに影響しているコントロールのみを収集すると簡単に最適化できるでしょう。できれば、ViewStateStats.axd ハンドラで切り替え可能なオプションにします。

HTML と JavaScript のエキスパートである場合は、確実に ViewStateStats.axd ページの表示を改善できます。最も簡単な表示形式を選択しましたが、コントロールの表示を展開したり折りたたんだりできるようにすると、非常に大規模な Web サイトのデータを簡単に表示できるようになるでしょう。

最後に、個人的なことですが、10 年間 Bugslayer コラムをご愛読くださっている読者の皆様に心より感謝申し上げます。何年もの間ご意見やご質問をお寄せいただいているおかげで、本当に楽しくコラムを執筆できています。

ヒント 80 最近、IIS のアプリケーション プールが予期せずリサイクルされるという問題に取り組んでいました。必死にインターネットで検索していると、Johan Straarup の「アプリケーション プールが予期せずリサイクルされる一般的な原因」 (blogs.msdn.com/johan/archive/2007/05/16/common-reasons-why-your-application-pool-may-unexpectedly-recycle.aspx) というすばらしいブログ エントリがヒットしました。W3WP.EXE の動作がおかしくなった原因がすべて見事に概説してあります。Johan のブログはぜひ購読してください。

ヒント 81 デバッグのスキルを向上するには、Roberto Farah がまとめたデバッグに関する書籍の一覧 (blogs.msdn.com/debuggingtoolbox/archive/2007/06/08/recommended-books-how-to-acquire-or-improve-debugging-skills.aspx) を参照してください。Roberto は WinDBG のエキスパートで、この一覧は私が今までに見た中で最高の一覧の 1 つです。Windows でどのような開発を行っていても、この一覧でカバーできます。Roberto はブログで、すばらしい WinDBG スクリプトで作業を行っています。私が個人的に気に入っているスクリプトは、マインスイーパでイカサマができる WinDBG スクリプト (blogs.msdn.com/debuggingtoolbox/archive/2007/03/28/windbg-script-playing-with-minesweeper.aspx) です。今では毎回勝っています。

ヒント 82 これまでの Bugslayer コラムを読んでいる方は、私が MSBuild タスク フリークであることをご存知でしょう (msdn.microsoft.com/msdnmag/issues/06/03/Bugslayer)。MSBuild を使用して環境を自動化するべきです。SDC Tasks が更新され、300 以上のタスクが含まれるようになりました。タスクには、Web サイト、アプリケーション プール、および Active Directory ユーザーの作成、FxCop の実行、仮想サーバーの構成、ZIP ファイルの作成、COM+ の構成、フォルダ共有の作成、GAC へのインストール、SQL Server の構成、BizTalk® Server 2004 と BizTalk Server 2006 の構成などがあります。自動化されたビルドを使用して生活を思いどおりにコントロールするには、codeplex.com/sdctasks から新しいタスクをダウンロードしてください。

ご意見やご質問は、

John slayer@microsoft.com.

まで英語でお送りください。
John Robbins は、.NET と Windows プラットフォームを専門とするソフトウェアのコンサルティング、教育、開発を行う Wintellect 社の共同設立者です。最新の著書は『Debugging Microsoft .NET 2.0 Applications』 (Microsoft Press、2006 年) です。連絡先は wintellect.com です。


同時実行の問題: C# で簡素化された APM

同時実行の問題
C# で簡素化された APM
Jeffrey Richter

コンテンツ
匿名メソッドとラムダ式
foreach ステートメント
反復子
非同期プログラミング

私は長い間、非同期プログラミング モデル (APM) の利点を賞賛し、I/O 関連の操作を非同期に実行することがスケーラブルで応答性の高いアプリケーションやコンポーネントを作成する鍵になると強調してきました。この目標は、APM を利用して達成できます。APM では、非常に少ないスレッドを使用して、いずれのスレッドもブロックせずに膨大な作業を実行できるためです。残念ながら、APM を使用して通常のアプリケーションやコンポーネントを構築するのは難しいので、多くのプログラマが敬遠しています。

APM の実装が困難な理由はいくつかあります。1 つ目の理由は、スレッドのスタック上のデータは別のスレッドに移行できないので、状態データをスレッドのスタック上に配置しないようにする必要があることです。スタック上にデータを配置しないということは、メソッド引数やローカル変数を使わないようにする必要があるということです。引数やローカル変数を使用するとプログラミングがずっと簡単になるので、開発者はここ数年好んで使用するようになっています。

2 つ目の理由は、コードをコールバック メソッドに分割する必要があることです (継続と呼ばれることもあります)。たとえば、あるメソッドで非同期の読み取り操作または書き込み操作を開始してから、(場合によっては別のスレッドによって) 呼び出される別のメソッドを実装して I/O 操作の結果を処理する必要があります。プログラマは、データ処理についてこのように考えることに慣れていません。メソッド間およびスレッド間で状態を移行するということは、状態をパッケージ化する必要があるということを意味し、結果として実装が複雑になります。

3 つ目の理由は、プログラミング構成要素には try/catch/finally ステートメント、lock ステートメント、using ステートメント、ループ (for、while、および foreach ステートメント) などの便利なものが多数ありますが、これらの構成要素は複数のメソッド間で分割できないということです。これらの構成要素を使用しないことも、実装が複雑になる原因になります。

最後の理由は、複数の重複操作の結果の調整、キャンセル、タイムアウト、Windows® Forms または Windows Presentation Foundation (WPF) アプリケーションの GUI スレッドに対する UI コントロールの変更のマーシャリングなどの機能を提供しようとすると、APM の使用がさらに複雑になることです。

このコラムでは、APM を非常に簡単に操作できるようにする C# プログラミング言語の比較的最近の追加機能について説明します。次に、私が独自に考案した AsyncEnumerator というクラスについて説明します。このクラスは、先ほど説明した問題を解決するために、この C# 言語機能に基づいて構築されています。AsyncEnumerator クラスを使用すると、コードで APM を簡単に楽しく使用できます。このクラスがあれば、非同期プログラミングを使用しない理由はなくなり、スケーラブルで応答性の高いコードを記述できるようになります。なお、AsyncEnumerator クラスは Power Threading Library に収録されており、このライブラリに収録されている他のコードに拠っています。このライブラリは、Wintellect.com からダウンロードできます。

匿名メソッドとラムダ式

図 1 の SynchronousPattern メソッドは、ファイルを同期的に開いて読み取る方法を示しています。このメソッドは単純です。FileStream オブジェクトを作成して Byte[] を割り当て、Read を呼び出して返されたデータを処理します。C# の using ステートメントによって、データの処理の完了後に FileStream オブジェクトが閉じられます。

Figure 1 ApmPatterns クラスの抜粋 (Close Figure 1)
private static void SynchronousPattern() {
    using (FileStream fs =
        new FileStream(typeof(ApmPatterns).Assembly.Location,
        FileMode.Open, FileAccess.Read, FileShare.Read, 8192,
        FileOptions.Asynchronous)) {
    Byte[] data = new Byte[fs.Length];
    Int32 bytesRead = fs.Read(data, 0, data.Length);
    ProcessData(data);
    }
}

#region APM Pattern with Multiple Methods
private sealed class ApmData {
    public FileStream m_fs;
    public Byte[] m_data;
}

private static void ApmPatternWithMultipleMethods() {
    ApmData apmData = new ApmData();
    apmData.m_fs =
        new FileStream(typeof(ApmPatterns).Assembly.Location,
        FileMode.Open, FileAccess.Read, FileShare.Read, 8192,
        FileOptions.Asynchronous);
    apmData.m_data = new Byte[apmData.m_fs.Length];
    apmData.m_fs.BeginRead(apmData.m_data, 0, apmData.m_data.Length,
        ReadCompleted, apmData);
    // Method returns after issuing async I/O request
}

private static void ReadCompleted(IAsyncResult result) {
    ApmData apmData = (ApmData) result.AsyncState;
    Int32 bytesRead = apmData.m_fs.EndRead(result);
    ProcessData(apmData.m_data);
    apmData.m_fs.Close(); // No 'using'
}
#endregion

private static void ApmPatternWithAnonymousMethod() {
    FileStream fs =
        new FileStream(typeof(ApmPatterns).Assembly.Location,
        FileMode.Open, FileAccess.Read, FileShare.Read, 8192,
        FileOptions.Asynchronous);
    Byte[] data = new Byte[fs.Length];
    fs.BeginRead(data, 0, data.Length, delegate(IAsyncResult result) {
        Int32 bytesRead = fs.EndRead(result);
        ProcessData(data);
        fs.Close(); // No 'using'
    }, null);
    // Method returns after issuing async I/O request
}

private static void ApmPatternWithLambdaExpression() {
    FileStream fs =
        new FileStream(typeof(ApmPatterns).Assembly.Location,
        FileMode.Open, FileAccess.Read, FileShare.Read, 8192,
        FileOptions.Asynchronous);
    Byte[] data = new Byte[fs.Length];
    fs.BeginRead(data, 0, data.Length, result => {
        Int32 bytesRead = fs.EndRead(result);
        ProcessData(data);
        fs.Close(); // No 'using'
    }, null);
    // Method returns after issuing async I/O request
}

ApmPatternWithMultipleMethods メソッドは、共通言語ランタイム (CLR) の APM を使用して SynchronousPattern メソッドと同じ操作を実行する方法を示しています。実装がはるかに複雑であることがすぐにわかるはずです。ApmPatternWithMultipleMethods メソッドによって非同期 I/O 操作が開始され、操作の完了時に ReadCompleted メソッドが呼び出されます。また、2 つのメソッド間でデータを渡すために、共有データを ApmData クラスのインスタンスにパッケージ化します。このクラスは、特にこれら 2 つのメソッド間でこのデータを渡せるようにするために定義する必要がありました。さらに、FileStream を開くメソッドと閉じるメソッドが異なるので、C# の using ステートメントは使用できません。これを補正するために、ReadCompleted メソッドから制御が戻る直前に FileStream の Close メソッドを明示的に呼び出すコードを記述する必要がありました。

ApmPatternWithAnonymousMethod メソッドは、匿名メソッドと呼ばれる新しい C# 2.0 機能を使用してこのコードを書き換える方法を示しています。この機能を使用すると、コードを引数としてメソッドに渡すことができます。あるメソッドのコードを別のメソッドのコード内に効果的に埋め込むことができるようになります (匿名メソッドの詳細については、私の著書『プログラミング Microsoft .NET Framework 第 2 版』 (日経 BP ソフトプレス、2006 年) を参照してください)。匿名メソッドの操作に慣れると、ApmPatternWithAnonymousMethod メソッドははるかに簡潔で理解しやすいものになります。

第 1 に、コードがすべて 1 つのメソッド内に含まれているので、コードが簡単になります。このコードでは、非同期 I/O 操作を開始するために BeginRead メソッドを呼び出しています。すべての BeginXxx メソッドは、最後から 2 番目のパラメータとして AsyncCallback を受け取ります。AsyncCallback は、操作の完了時にスレッド プールのスレッドによって呼び出されるメソッドを参照するデリゲートです。通常、APM を使用する場合は、個別のメソッドを記述してそのメソッドに名前を付け、BeginXxx メソッドの最後の引数を使用してメソッドに追加データを渡す必要があります。ただし、匿名メソッド機能を使用すると、要求を開始して結果を処理するすべてのコードがコンテキスト内に含まれるように個別のメソッドをインラインで記述するだけで済みます。実際には、コードは SynchronousPattern メソッドにやや似ています。

第 2 に、ApmData クラスが不要になっています。このクラスを定義してそのインスタンスを作成し、フィールドを操作する必要はありません。その理由は次のとおりです。匿名メソッドでできることは、あるメソッドのコードを別のメソッドのコード内に埋め込むことだけではありません。C# コンパイラは、外部メソッドで宣言され内部メソッドでも使用される引数またはローカル変数を検出すると、実際にはクラスを自動的に定義します。2 つのメソッド間で共有される各変数が、このコンパイラによって定義されたクラスのフィールドになります。次に、ApmPatternWithAnonymousMethod メソッド内で、このクラスのインスタンスを作成するコードがコンパイラによって生成されます。変数を参照するコードは、コンパイラによって定義されたクラスのフィールドにアクセスするコードにコンパイルされます。また、内部メソッドが新しいクラスのインスタンス メソッドになります。これにより、コードがフィールドに簡単にアクセスできるようになり、2 つのメソッドでデータを共有できるようになります。

これが匿名メソッドのすばらしいところです。メソッド引数やローカル変数を使用しているように見えるコードを記述できますが、実際はコンパイラによってコードが書き換えられ、これらの変数がスタックから取り出されて、オブジェクトにフィールドとして埋め込まれています。オブジェクトはメソッド間で簡単に渡すことができ、スレッド間で簡単に移行することができます。この点が APM を使用する場合に最適です。また、この作業はすべてコンパイラによって自動的に実行されるので、メソッド間およびスレッド間で明示的に渡す必要があるデータがなくなり、BeginRead メソッドの最後のパラメータに null を渡すだけで済みます。ただし、1 つのメソッドのみが存在するように見えても 2 つの異なるメソッドが存在しているので、C# の using ステートメントは使用できません。

次に、図 1 に抜粋したコードの実行後の出力を示します。

Primary ThreadId=1
ThreadId=1: 4D-5A-90-00-03 (SynchronousPattern)
ThreadId=3: 4D-5A-90-00-03 (ApmPatternWithMultipleMethods)
ThreadId=3: 4D-5A-90-00-03 (ApmPatternWithAnonymousMethod)
ThreadId=3: 4D-5A-90-00-03 (ApmPatternWithLambdaExpression)
ThreadId=3: 4D-5A-90-00-03 (ApmPatternWithIterator)

Main メソッドによって、アプリケーションのプライマリ スレッドのマネージ スレッド ID が表示されるようにしました。その後、ProcessData メソッドによって、実行元のスレッドのマネージ スレッド ID が表示されるようにしています。ご覧のとおり、すべての非同期パターンの結果がプライマリ スレッド以外のスレッドによって実行されており、同期パターンではすべてがアプリケーションのプライマリ スレッドによって実行されていることが、この出力からわかります。

また、C# 3.0 でラムダ式と呼ばれる新機能が導入されているということも指摘しておきます。ラムダ式機能は、C# の匿名メソッド機能と同じ処理を実行する、より簡潔な構文です。実は、この機能は C# チームにとって厄介な問題になっています。同じ結果が得られる 2 つの異なる構文をドキュメント化し、サポートする必要があるためです。C# 3.0 のラムダ式機能を使用するように ApmPatternWithAnonymousMethod メソッドを変更すると、図 1 に示す ApmPatternWithLambdaExpression メソッドになります。コンパイラが result 引数の型を IAsyncResult と自動的に推測でき、"delegate" より "=>" の方が入力が少ないので、構文がやや簡潔です。

foreach ステートメント

C# 2.0 では、C# プログラミング言語に反復子という機能も導入されています。反復子機能の当初の目的は、コレクションの内部データ構造をスキャンするコードを開発者が簡単に記述できるようにすることでした。反復子について理解するには、まず C# の foreach ステートメントの詳細を確認する必要があります。次のようなコードを記述すると、コンパイラによって図 2 に示すようなコードに変換されます。

Figure 2 C# の foreach ループ (Close Figure 2)
// Construct an object that knows how to traverse the
// internal data structure of the collection
IEnumerator<String> e = collectionObject.GetEnumerator();
try {
   String s;
   while (e.MoveNext()) { // Advance to the next item
      s = e.Current;      // Get the item to process
      DoSomething(s);
   }
}
finally {
   // If the IEnumerable implements IDisposable,
   // clean up the object
   IDisposable d = e as IDisposable;
   if (d != null) d.Dispose();
}

foreach (String s in collectionObject)
   DoSomething(s);

foreach ステートメントを使用すると、コレクション クラスのすべてのアイテムを簡単に反復処理できます。ただし、コレクション クラスには配列、リスト、辞書、ツリー、リンク リストなどのさまざまな種類があり、それぞれが独自の内部データ構造を使用してアイテムのコレクションを表します。foreach ステートメントを使用することは、コレクション内のすべてのアイテムを反復処理することを意味し、アイテムを保持するためにコレクション クラスで内部的に使用されるデータ構造については問わないことを意味します。

foreach ステートメントを使用すると、コンパイラによってコレクション クラスの GetEnumerator メソッドの呼び出しが生成されます。このメソッドでは、IEnumerator<T> インターフェイスを実装するオブジェクトが作成されます。このオブジェクトは、コレクションの内部データ構造をスキャンする方法を認識しています。while ループの繰り返しごとに、IEnumerator<T> オブジェクトの MoveNext メソッドが呼び出されます。このメソッドは、コレクション内の次のアイテムに進むようにオブジェクトに指示し、成功した場合は true を、すべてのアイテムが列挙された場合は false を返します。内部的には、MoveNext は単にインデックスをインクリメントしたり、リンク リストの次のノードに進んだり、ツリーを上方向または下方向にスキャンしたりします。MoveNext メソッド全体のポイントは、foreach ループを実行しているコードからコレクションの内部データ構造を抽象化することです。

MoveNext が true を返した場合、Current プロパティの get アクセサ メソッドを呼び出すと、MoveNext メソッドが進んだ先のアイテムを foreach ステートメントの本文 (try ブロック内のコード) で処理できるように、そのアイテムの値が返されます。コレクション内のすべてのアイテムが処理されたと判断した場合、MoveNext は false を返します。

この時点で、while ループが終了して finally ブロックに入ります。finally ブロックでは、IEnumerator<T> オブジェクトが IDisposable インターフェイスを実装しているかどうかが確認され、実装している場合はその Dispose メソッドが呼び出されます。実際には、IEnumerator<T> インターフェイスは IDisposable から派生しているので、すべての IEnumerator<T> オブジェクトで Dispose メソッドが実装されている必要があります。列挙子オブジェクトの中には、反復処理中に追加リソースが必要になるものもあります。オブジェクトがテキスト ファイルからテキスト行を返していることなどが考えられます。foreach ループの終了時に (コレクション内のすべてのアイテムを反復処理する前に終了することもあります)、finally ブロックで Dispose が呼び出され、IEnumerator<T> オブジェクトがこのような追加リソースを解放できるようになります (たとえば、テキスト ファイルを閉じます)。

反復子

foreach ステートメントで生成されるコードを確認したところで、オブジェクトで IEnumerable<T> インターフェイスと共にその MoveNext および Current メンバを実装する方法について見てみましょう。すべてのコレクション データ構造の中で配列がおそらく最も簡単なので、配列について説明します。すべての配列の基本クラスである System.Array クラスは、IEnumerable<T> インターフェイスを実装します。GetEnumerator メソッドは、IEnumerator<T> インターフェイスを実装する型を持つオブジェクトを返します。そのため、配列のすべての要素を反復処理するコードを簡単に記述できます。

Int32[] numbers = new Int32[] { 1, 2, 3 };
foreach (Int32 num in numbers)
   Console.WriteLine(num);

補足になりますが、foreach で使用されるコレクションが配列であると検出すると、C# コンパイラは、0 から配列の長さマイナス 1 の値まで進む簡単なインデックスを含む最適化されたコードを生成します。ただし、ここでは、コンパイラによってこの最適化が行われることは無視し、配列はその他のコレクション クラスと同様に扱われるということにしましょう。実際、次のように IEnumerable<T> に配列をキャストして、コンパイラで配列がその他のコレクション クラスと同様に扱われるようにすることができます。

foreach (Int32 num in (IEnumerable<Int32>) numbers)
   Console.WriteLine(num);

この ApmPatterns クラスには、配列内の各アイテムを逆順に返す方法を認識している ArrayEnumerator<T> クラスが含まれています (図 3 を参照)。このクラスは IEnumerator<T> インターフェイスを実装するので、クラスで非ジェネリックの IEnumerable および IDisposable インターフェイスも実装する必要があります。AsyncEnumerator<T> クラスには、渡された配列をプライベート メンバに保存するパブリック コンストラクタが用意されています。現在反復処理しようとしている配列内のアイテムを示す別のプライベート メンバ m_index もあります。このフィールドは、配列の長さ (配列内の最後の要素を過ぎた要素) に初期化されます。図 3 を参照してください。

Figure 3 反復子 (Close Figure 3)
private static void Iterators() {
    Int32[] numbers = new Int32[] { 1, 2, 3 };

    // Use Array's built-in IEnumerable
    foreach (Int32 num in numbers)
    Console.WriteLine(num);

    // Use my ArrayEnumerable class
    foreach (Int32 num in new ArrayEnumerable<Int32>(numbers))
    Console.WriteLine(num);

    // Use my iterator
    foreach (Int32 num in ArrayIterator(numbers))
    Console.WriteLine(num);
}

private sealed class ArrayEnumerator<T>: IEnumerator<T> {
    private T[] m_array;
    private Int32 m_index;

    public ArrayEnumerator(T[] array) {
        m_array = array; m_index = array.Length;
    }
    public Boolean MoveNext() {
        if (m_index == 0) return false;
        m_index--; return true;
    }
    public T Current { get { return m_array[m_index]; } }
    Object IEnumerator.Current { get { return Current; } }
    public void Dispose() { }
    public void Reset() { }
}

private sealed class ArrayEnumerable<T>: IEnumerable<T> {
    private T[] m_array;
    public ArrayEnumerable(T[] array) { m_array = array; }
    public IEnumerator<T> GetEnumerator() {
        return new ArrayEnumerator<T>(m_array);
    }
    IEnumerator IEnumerable.GetEnumerator() {
        return GetEnumerator();
    }
}

private static IEnumerable<T> ArrayIterator<T>(T[] array) {
    try { yield return default(T); }
    finally {  }
    for (Int32 index = array.Length - 1; index >= 0; index--)
    yield return array[index];
}

private static IEnumerator<Int32>
    ApmPatternWithIterator(AsyncEnumerator ae) {

    using (FileStream fs =
        new FileStream(typeof(ApmPatterns).Assembly.Location,
        FileMode.Open, FileAccess.Read, FileShare.Read, 8192,
        FileOptions.Asynchronous)) {

        Byte[] data = new Byte[fs.Length];
        fs.BeginRead(data, 0, data.Length, ae.End(), null);
        yield return 1;

        Int32 bytesRead = fs.EndRead(ae.DequeueAsyncResult());
        ProcessData(data);
    }
}

実質的に、IEnumerator<T> オブジェクトはすべて、どのアイテムをコレクションから列挙するかを追跡するステート マシンです。foreach ループが MoveNext を呼び出すと、MoveNext メソッドがステート マシンを次のアイテムに進めます。この ArrayEnumerator クラスでは、すべてのアイテムが列挙された場合 (m_index が既に 0 で、最後のアイテムである場合) は MoveNext が false を返します。これに該当しない場合は、m_index フィールドがデクリメントされ、true が返されます。Current プロパティは、単にステート マシンの現在の位置が反映されたアイテムを返します。

ご覧のとおり、このコードは実装が面倒なので、コードを簡略化するために、いくつかのインターフェイス メソッドを空にしています。また、いくつかのエラー チェック コードも省略しています。さらに、配列は非常に単純なデータ構造なので、その列挙子の記述はかなり簡単です。リンク リストやツリーなどのその他のデータ構造の場合、コードは大幅に複雑になります。

ここで指摘しておきたいことがもう 1 つあります。ArrayEnumerator<T> クラスは、foreach ステートメントで直接使用できません。つまり、次のコードはコンパイルされません。

foreach (Int32 num in new ArrayEnumerator<Int32>(numbers))
   Console.WriteLine(num);

foreach ステートメントでは、in キーワードの後に、IEnumerator<T> ではなくコレクション (IEnumerable<T> を実装する型を持つオブジェクト) が指定されることが想定されています。そのため、ArrayEnumerator<T> を使用するには、IEnumerable<T> を実装するクラスである ArrayEnumerator<T> クラス (図 3 を参照) も定義する必要がありました。ArrayEnumerable<T> クラスを定義することによって、適切にコンパイルおよび実行される次のコードを記述できます。

foreach (Int32 num in new ArrayEnumerable<Int32>(numbers))
   Console.WriteLine(num);

いかかでしょう。アイテムのコレクションを反復処理するには、相当量のコードを記述する必要があります。このコードすべてをもっと簡単に記述できるテクノロジがあったならば、と考えずにはいられません。さいわいなことに、このようなテクノロジは存在します。C# 2.0 の反復子機能です。反復子を使用すると、順序どおりに並べられた値を返す単一のメンバを記述できます。簡単な構文を使用してこのメンバを記述し、値を返す方法を示すことができます。記述したコード ブロックは、C# コンパイラによって IEnumerable<T>、IEnumerable、IEnumerator<T>、IEnumerator、および IDisposable インターフェイスを実装する完全なクラスに変換されます。

C# の反復子機能を使用して、AsyncEnumerable<T> クラスと AsyncEnumerator<T> クラスを次の 1 つのメンバに置き換えます。

private static IEnumerable<T> ArrayIterator<T>(T[] array) {
   for (Int32 index = array.Length-1; index >= 0; index--)
      yield return array[index];
}

このメンバは、次のように foreach ステートメントを使用して呼び出すことができます。

foreach (Int32 item in ArrayIterator(numbers))
   Console.WriteLine(item);

さらに、これは (ジェネリック クラスに対する) ジェネリック メソッドであり、C# の型の推定も行われるため、ArrayIterator<Int32> を記述する必要がなく、foreach コードもより簡単になります。

なお、この反復子のメンバは IEnumerable<T> を返すので、IEnumerable<T> および IEnumerable を実装し、IEnumerator<T>、IEnumerator、および IDisposable を実装するコードがコンパイラによって生成されます。ただし、IEnumerator<T> を返す反復子のメンバを記述することもできます。この場合、IEnumerator<T>、IEnumerator、および IDisposable インターフェイスのメンバのみを実装するクラス定義がコンパイラによって生成されます。

また、戻り値の型として非ジェネリックの IEnumerable または IEnumerator インターフェイスを持つ反復子のメンバを定義できるということも指摘しておきます。この場合、コンパイラによって IEnumerable<Object>/IEnumerable (戻り値の型として IEnumerable を指定した場合)、IEnumerator<Object>/IEnumerator、および IDisposable が定義されます。

反復子では、yield return ステートメントによって、メソッドから返す必要がある値 (コレクション アイテム) がコンパイラに効果的に指示されます。ただし、この時点では、反復子はその実行を実際には終了しません。代わりに、その実行は中断されます。次回 MoveNext が呼び出されたときに、反復子は、yield return ステートメントの直後のステートメントからその実行を再開します。yield return に加えて、反復子には yield break ステートメントを含めることができます。このステートメントを実行すると、MoveNext メソッドが false を返し、foreach ループを強制終了します。反復子のメンバを終了した場合も、MoveNext メソッドが false を返し、foreach ループを強制終了します。

非同期プログラミング

非同期プログラミングで反復子を使用すると、大きなメリットがいくつか得られます。第 1 に、反復子内で、引数およびローカル変数にアクセスするコードを記述できます。ただし、実際には、コンパイラによってこれらの変数がクラスのフィールドにパッケージ化されています (匿名メソッドやラムダ式の場合と同様です)。

第 2 に、コンパイラで逐次コードを記述できますが、メソッドを途中で中断し、その実行を後で続行することができます (別のスレッドを使用して続行できます)。

第 3 に、try/catch/finally ステートメント、lock ステートメント、using ステートメント、ループ (for、while、および foreach ステートメント) などのプログラミング構成要素を反復子内で使用できます。これらの構成要素でセマンティクスが維持されるように、コンパイラによってコードが自動的に書き換えられます。

念のため、反復子に関連する制限事項を示しておきます。

  • 反復子のメンバのシグネチャには、out または ref パラメータを含めることはできません。
  • 反復子には、return ステートメントを含めることはできません (yield return は含めることができます)。
  • 反復子には、アンセーフ コードを含めることはできません。
  • finally ブロックには、yield return または yield break ステートメントを含めることはできません。
  • yield return ステートメントは、catch ブロックを含む try ブロックには挿入できません (yield break は挿入できます)。
  • yield return ステートメントは、catch ブロックには挿入できません (yield break は挿入できます)。

これらの制限事項を覚えておくことをお勧めしますが、ほとんどのプログラミング シナリオでは、これらの制限事項はまったく問題にならないか、対処が可能です。

これで、この反復子の知識を活用して、一連の非同期操作を実行する反復子を作成できます。図 3 の ApmPatternWithIterator メンバを参照してください。メンバの戻り値の型が IEnumerator<T> なので、このメンバが反復子です。このメンバのコードは、既に説明した SynchronousPattern メソッドに似ています。具体的には、反復子に using ステートメントを含める方法や、引数 (ae) およびローカル変数 (fs および data) の使用方法に注目してください。

このメンバは (Read を呼び出すだけでなく) BeginRead および EndRead を呼び出すので、このメンバによって非同期操作が実行されることがわかります。ただし、APM を使用する場合は通常、BeginRead を呼び出してコールバック メソッドを渡します。この場合は、ae.End の呼び出し結果を渡しています (これについては、次回のコラムで説明します)。通常は、BeginRead を呼び出した後にメソッドから制御が戻りますが、反復子では、yield return を呼び出してコードを中断しています。

APM を使用する場合は通常、EndRead の呼び出しは別のメソッドで行われますが、反復子では、EndRead の呼び出しは yield return ステートメントの直後に行われます。つまり、反復子の実行が再開されると呼び出しが行われます。また、このコードは、別のスレッドで実行できます。実際に、前述の出力を見てみると、ApmPatternWithIterator の ProcessData の呼び出しは、アプリケーションのプライマリ スレッド (ThreadId=1) とは異なるスレッド (ThreadId=3) によって実行されていることがわかります。

このコードをよく観察すると、ここで行われていることは実にすばらしく、非同期プログラミングが同期プログラミングと同じくらい簡単に実行されていることがわかります。さらに、スレッドがブロックされることはなく、アプリケーションのユーザーへの応答性を維持することができます。スレッドがブロックされないので、追加のスレッドを作成する必要がなく、アプリケーションで使用されるリソースは非常に少なくなります。

複数の反復子を同時に実行して、アプリケーションのスケーラビリティを向上することができます。また、未処理の操作のキャンセル機能、進行状況の報告機能、タイマの設定機能、複数の重複操作の結果の調整機能、および UI の更新が必要な場合にこのすべてを GUI スレッドで実行する機能を簡単に追加できます。さらに、より便利な同期プログラミング モデルによく似たプログラミング モデルを使用して、このすべてを実現できます。

このすべてを実際に可能にしているのは、C# の反復子とその反復子を実行するために私が開発した AsyncEnumerator クラスです。皆さんに AsyncEnumerator クラスの詳細とその動作をどうしても知りたいと思っていただけるくらいに盛り上げることができたことを願っています。残念ながら、今月のコラムは紙幅が尽きてしまいましたので、次回のコラムで AsyncEnumerator クラスの詳細について説明したいと思います。

ご意見やご質問は、

Jeffrey mmsync@microsoft.com.

まで英語でお送りください。
Jeffrey Richter は、アーキテクチャのレビュー、トレーニング、およびコンサルティング サービスを提供する Wintellect 社 (www.Wintellect.com) の共同設立者です。彼の著書には『プログラミング Microsoft .NET Framework 第 2 版』 (日経 BP ソフトプレス、2006 年) などがあります。Jeffrey は、MSDN Magazine の寄稿編集者で、1990 年からマイクロソフトのコンサルティングを担当しています。


..NET の問題: ファイナライザをデバッグする

..NET の問題
ファイナライザをデバッグする
Stephen Toub

Q さまざまな理由により、IDisposable を実装する必要があるカスタム型が多数あります。このクラスを使用しているチームの他の開発者がこれらの型を常に正しく破棄するようにしたいと考えています。チームのメンバのだれかが Dispose の呼び出しを忘れた場合に警告を表示するには、どうすればよいでしょうか。

A まず、コードに対して静的分析を実行する場合は、Visual Studio® 2005 と FxCop がこれに役立ちます。msdn2.microsoft.com/ms182289 で説明されている規則 CA2000 は、"スコープが失われる前にオブジェクトを破棄します" であり、ローカル IDisposable オブジェクトが作成された後、そのオブジェクトへのすべての参照がスコープ外になる前に破棄されたかどうかをチェックします。これは便利ですが、静的分析によって検出できる場合は限られているため、完全な解決策ではありません。実際に必要とされているのは、カスタム型を使用している開発者に対して、それらの型が破棄されずにガベージ コレクトされた場合に警告を表示する方法です。これを行うために、ファイナライザを利用できます。

一般的には、どうしても必要でない限り、型にファイナライザを実装することは避ける必要があります。Microsoft® .NET Framework 2.0 の SafeHandle では、どうしても必要となる理由はほとんどありません。ただし、IDisposable を実装する必要があるシナリオは数多くあります。これには、IDisposable を実装するマネージ リソースを型が所有するすべてのシナリオが含まれます。このようなシナリオでは、図 1 に示すように、含まれているリソースに対して Dispose を呼び出す Dispose メソッドを型で提供する必要があります (IDisposable の実装の詳細については、MSDN® Magazine 2007 年 7 月号の Shawn Farkas による「CLR 徹底解剖」コラム (msdn.microsoft.com/msdnmag/issues/07/07/CLRInsideOut) を参照してください)。

Figure 1 含まれているマネージ リソースを破棄する (Close Figure 1)
public class DisposableClass : IDisposable
{
    private SomeOtherDisposableClass _data = ...;

    public DisposableClass()
    {
    }

    public void Dispose()
    {
        Dispose(true);
        GC.SuppressFinalize(this); // if DisposableClass isn't sealed
    }

    protected virtual void Dispose(bool disposing)
    {
        if (disposing)
        {
            _data.Dispose();
        }
    }

    ...
}
Figure 1 含まれているマネージ リソースを破棄する (Close Figure 1)
public class DisposableClass : IDisposable
{
    private SomeOtherDisposableClass _data = ...;

    public DisposableClass()
    {
    }

    public void Dispose()
    {
        Dispose(true);
        GC.SuppressFinalize(this); // if DisposableClass isn't sealed
    }

    protected virtual void Dispose(bool disposing)
    {
        if (disposing)
        {
            _data.Dispose();
        }
    }

    ...
}

図 1 では、DisposableClass はネイティブ リソースを直接使用しないので、ファイナライザを提供する必要はありません (その Dispose メソッドは、派生型がファイナライザを実装している場合は GC.SuppressFinalize を呼び出します)。ただし、図 2 に示すように、デバッグ目的でファイナライザを追加することで、クラスが正しく破棄されなかったことを検出する手段を導入できます。型のインスタンスがすべて正しく破棄される限り、ファイナライザは呼び出されません。いずれかのインスタンスが破棄されない場合は、そのインスタンスがもう参照されないことが確認されてから、ファイナライザ スレッドでその Finalize メソッド (~DisposableClass) が呼び出されて、デバッガでアサーションが発生します。

Figure 2 デバッグするためにファイナライザを追加する (Close Figure 2)
public class DisposableClass : IDisposable
{
    private SomeOtherDisposableClass _data = ...;

    public DisposableClass()
    {
    }

    public void Dispose()
    {
        Dispose(true);
        GC.SuppressFinalize(this);
    }

    protected virtual void Dispose(bool disposing)
    {
        if (disposing)
        {
            data.Dispose();
        }
    }

#if DEBUG
    ~DisposableClass()
    {
        Debug.Fail("You forgot to Dispose this instance.");
    }
#endif

    ...
}
Figure 2 デバッグするためにファイナライザを追加する (Close Figure 2)
public class DisposableClass : IDisposable
{
    private SomeOtherDisposableClass _data = ...;

    public DisposableClass()
    {
    }

    public void Dispose()
    {
        Dispose(true);
        GC.SuppressFinalize(this);
    }

    protected virtual void Dispose(bool disposing)
    {
        if (disposing)
        {
            data.Dispose();
        }
    }

#if DEBUG
    ~DisposableClass()
    {
        Debug.Fail("You forgot to Dispose this instance.");
    }
#endif

    ...
}

残念ながら、カスタム型を使用している開発者が、インスタンスを破棄しなかったことやそれがどのインスタンスであるかはわかるようになりますが、そのインスタンスの作成元はわからない可能性があります。型が 1 か所のみでインスタンス化されている場合は、それで十分かもしれません。ただし、コード ベースの至るところで型がインスタンス化されている場合は、開発者はこの特定のインスタンスの作成元を知る手段がないので、このインスタンスがなぜ破棄されなかったかを把握することが難しくなります。

これに対処するため、構築時に追加情報をクラスに集めることができます。たとえば、次のメンバを追加して、このインスタンスが作成された時点のスタック トレース、作成元のスレッド、および作成時刻を追跡することもできます。

#if DEBUG
    private StackTrace _stack = new StackTrace(true);
    private int _threadId = Thread.CurrentThread.ManagedThreadId;
    private DateTime _time = DateTime.UtcNow;
    ...
#endif

また、ファイナライザが実行された場合にこれらのメンバからの情報を含めるように Debug.Fail を変更して、ユーザーがこのインスタンスの作成元を正確に判別し、インスタンスが正しく破棄されなかった原因を突き止められるようにすることもできます。前のコード スニペットのプラグマ ディレクティブ (#if および #endif) は、このコードのすべてが、RELEASE ビルドではなく DEBUG ビルドのみにコンパイルされるようにするために使用されることに注意してください。スタック トレースの取得は比較的負荷の高い操作であり、パフォーマンスが懸念される場合は、できる限り避ける必要があります。

図 2 では、ファイナライザを同じ形式で囲っていることもわかります。これは主にスタイルの問題です。ファイナライザは抑制されている場合でもシステムにオーバーヘッドを追加するので、適切な機能に必要がなければ、クラスで使用する意味はありません (割り当てられると、ファイナライズ可能なオブジェクトはファイナライズ リストに追加されます。これらのインスタンスにアクセスできなくなり、GC が実行されると、インスタンスはファイナライザ スレッドで処理される "FReachable" キューに移動されます。GC.SuppressFinalize を指定してファイナライズを抑制すると、オブジェクトが GC によって FReachable キューに移動されないように、"ファイナライザを実行しない" というフラグがオブジェクトのヘッダーに設定されます。したがって、ファイナライザが何も処理しない場合や抑制されている場合でも、ごくわずかですが、ファイナライザにオブジェクトを渡すオーバーヘッドがまだあります)。

これは個別の型には効果的ですが、同様の動作を必要とする多数の型を作成している場合は、このコードを再利用可能なクラスに抽出することがほとんどのコーディング手法で求められます。一見して、これは実に難しそうな問題です。ここからは、いくつかの異なる実装について、それぞれの長所と短所を示しながら説明します。ニーズに基づいて、それぞれの実装から選択できます。

すべての実装に共通する点の 1 つとして、構築時にさまざまな量の情報を格納する必要があります。そのため、カスタムの例外クラスを作成することにしました。この例外クラスは、構築されるとき、既に説明したようなデータを取得します。その後、ファイナライザを呼び出すときにこのクラスをスローできます。または、その ToString メソッドを Debug.Fail や同様のメソッドの呼び出しの引数として使用できます (.NET Framework 2.0 以降では、ファイナライザ スレッドから例外をスローすると、既定ではアプリケーションが終了することに注意してください。以前のバージョンではこれとは異なり、ファイナライザ スレッドからの例外はランタイムによって暗黙的に処理されます。ここでは例外をスローしませんが、オブジェクトを破棄しないことがプロセスを停止する必要がある深刻なエラーだと思われる場合は、該当する行のコメントを自由に外してください)。私が作成した InvokedFinalizerException を図 3 に示します。

Figure 3 InvokedFinalizerException (Close Figure 3)
[Serializable]
public class InvokedFinalizerException : Exception
{
    public readonly DateTime InstantiationTime = DateTime.UtcNow;
    public readonly int InstantiationThreadId =
        Thread.CurrentThread.ManagedThreadId;
    public readonly StackTrace InstantiationStackTrace =
        new StackTrace(true);

    public InvokedFinalizerException() :
        base("A finalizer was invoked.") { }

    public InvokedFinalizerException(string message) : base(message) { }

    public InvokedFinalizerException(string message, Exception inner) :
        base(message, inner) { }

    protected InvokedFinalizerException(
        SerializationInfo info, StreamingContext context) :
        base(info, context) { }

    public override string ToString()
    {
        return
            "Time: " + InstantiationTime + Environment.NewLine +
            "Thread: " + InstantiationThreadId + Environment.NewLine +
            "Stack: " + InstantiationStackTrace;
    }
}
Figure 3 InvokedFinalizerException (Close Figure 3)
[Serializable]
public class InvokedFinalizerException : Exception
{
    public readonly DateTime InstantiationTime = DateTime.UtcNow;
    public readonly int InstantiationThreadId =
        Thread.CurrentThread.ManagedThreadId;
    public readonly StackTrace InstantiationStackTrace =
        new StackTrace(true);

    public InvokedFinalizerException() :
        base("A finalizer was invoked.") { }

    public InvokedFinalizerException(string message) : base(message) { }

    public InvokedFinalizerException(string message, Exception inner) :
        base(message, inner) { }

    protected InvokedFinalizerException(
        SerializationInfo info, StreamingContext context) :
        base(info, context) { }

    public override string ToString()
    {
        return
            "Time: " + InstantiationTime + Environment.NewLine +
            "Thread: " + InstantiationThreadId + Environment.NewLine +
            "Stack: " + InstantiationStackTrace;
    }
}

例外クラスを配置したら、最初の方法では、図 4 に示すように追跡コードをすべて個別の FinalizationDebugger という構築可能なクラスに移動します。このクラスのインスタンスは、破棄可能なクラスのメンバとして格納され、クラスの構築時に構築されます。クラスを破棄すると、この個別の FinalizationDebugger クラスも破棄されます。クラスが破棄されない場合は、そのクラスが参照されなくなってから FinalizationDebugger が最終的に収集され、インスタンスが正しく破棄されなかったことや、その作成元などを把握するのに必要なすべての関連情報がそのファイナライザによって提供されます。FinalizationDebugger はオブジェクトへの参照を保持しますが、オブジェクトに含まれているのはこの FinalizationDebugger インスタンスへの参照のみであるため、オブジェクトへのその参照がオブジェクトの収集を妨げることはありません (.NET Framework ガベージ コレクタが、マーク アンド スイープ スキームを使用する代わりに参照カウント スキームで動作した場合、この循環は問題になる可能性があります)。もちろん、図 4 に示すように、破棄可能なクラスでこれを使用する場合はもう少し記述を追加する必要があります。実際には、これは、もっと簡潔な解決策にリファクタリングしようとしていた元のバージョンとあまり変わりません。

Figure 4 メンバ アプローチ (Close Figure 4)

個別のクラスへのリファクタリング

public sealed class FinalizationDebugger : IDisposable
{
    private InvokedFinalizerException _exc;
    private object _obj;

    public FinalizationDebugger(object obj)
    {
        _exc = new InvokedFinalizerException();
        _obj = obj;
    }

    public void Dispose()
    {
        GC.SuppressFinalize(this);
    }

    ~FinalizationDebugger()
    {
        Debug.Fail(_exc.ToString());
        // throw _exc;
    }
}

FinalizationDebugger を使用する
public class DisposableClass : IDisposable
{
    private SomeOtherDisposableClass _data = ...;
#if DEBUG
    private FinalizationDebugger _fd;
#endif

    public DisposableClass()
    {
#if DEBUG
       _fd = new FinalizationDebugger(this);
#endif
    }

    public void Dispose()
    {
        Dispose(true);
        GC.SuppressFinalize(this);
    }

    protected virtual void Dispose(bool disposing)
    {
        if (disposing)
        {
            _data.Dispose();
#if DEBUG
            _fd.Dispose();
#endif
        }
    }
}

次の方法では、破棄可能なクラスを派生できる基本クラスにこのコードをすべて移動することで一歩前進します。この方法を図 5 に示します。これによって破棄可能なクラスが簡潔になることがわかります。必要な変更は、型の基本クラスとしての FinalizationDebuggerBase の追加、パラメータなしの Dispose メソッドの削除 (基本クラスで既に実装されているため)、および基本実装をオーバーライドするように Dispose(bool) メソッドを変更することだけです。

Figure 5 基本クラス アプローチ (Close Figure 5)

基本クラスへのリファクタリング

public class FinalizationDebuggerBase : IDisposable
{
#if DEBUG
    private FinalizationDebugger _fd;
#endif

    public FinalizationDebuggerBase()
    {
#if DEBUG
        _fd = new FinalizationDebugger(this);
#endif
    }

    public void Dispose() { Dispose(true); }

    protected virtual void Dispose(bool disposing)
    {
#if DEBUG
        if (disposing) _fd.Dispose();
#endif
    }
}

FinalizationDebuggerBase を使用する

public class DisposableClass : FinalizationDebuggerBase
{
    private SomeOtherDisposableClass _data = ...;

    public DisposableClass() {}

    protected override void Dispose(bool disposing)
    {
        try
        {
            if (disposing) _data.Dispose();
        }
        finally { base.Dispose(disposing); }
    }
}

ただし、この方法にはいくつかの問題があります。第 1 に、#if および #endif プラグマ ディレクティブは、コンパイル時、FinalizationDebuggerBase がコンパイルされるときに評価されます。FinalizationDebuggerBase が DisposableClass と同じプロジェクトの一部である場合は、常にクラスと同じ DEBUG コンパイル フラグまたは RELEASE コンパイル フラグが選択され、同期が維持されます。これは、ほぼ期待どおりの動作です。ただし、FinalizationDebuggerBase が別のアセンブリ内にある場合 (たとえば、会社の多数のプロジェクトで共有されるアセンブリ内にコンパイルされる場合)、参照するそのアセンブリは既にコンパイルされていることになるので、このプロジェクトの DEBUG または RELEASE の選択は無視されます。したがって、プロジェクトを RELEASE モードでコンパイルして、このファイナライズ コードがまったく組み込まれないようにすることもできますが、FinalizationDebuggerBase クラスを DEBUG モードでコンパイルした場合は、期待した動作にはなりません。第 2 の問題はもっと明確で、.NET では複数のクラス継承がサポートされないので、クラスは 1 つしか基本クラスを持つことができないという問題です (ただし、インターフェイスはいくつでも実装できます)。つまり、クラスに基本クラスが既にある場合、この方法は使用できません (この基本クラスを何らかの形で階層のどこかに挿入できる場合を除く)。

3 番目の方法では、インスタンスで呼び出す静的クラスにすべて抽出します。この実装を図 6 に示します。この方法では、FinalizationDebugger は、Constructor、Dispose、および Finalizer の 3 つの静的メソッドを公開する静的クラスです。これらのメソッドをクラスの適切な場所から呼び出すという考え方で、その適切な場所はメソッドの名前から明らかでしょう (例については、図 6 を参照)。これは、通常は 3 行のコードを追加する必要があるだけで、コードへの影響はごくわずかです (ただし、型にファイナライザがまだない場合は、それらの数行も追加する必要があります)。これらのメソッドはすべて、クラスを DEBUG モードでコンパイルした場合にクラスで呼び出されるだけにするように、ConditionalAttribute でマークされています。

Figure 6 静的クラス アプローチ (Close Figure 6)

静的クラスへのリファクタリング

public static class FinalizationDebugger<T> where T : class
{
    private static Dictionary<object, InvokedFinalizerException> _db =
        new Dictionary<object, InvokedFinalizerException>();

    [Conditional("DEBUG")]
    public static void Constructor(T obj)
    {
        if (obj == null) throw new ArgumentNullException("obj");
        ObjectEqualityWeakReference weakRef =
            new ObjectEqualityWeakReference(obj);
        InvokedFinalizerException exc = new InvokedFinalizerException();
        lock (_db) _db.Add(weakRef, exc);
    }

    [Conditional("DEBUG")]
    public static void Dispose(T obj)
    {
        if (obj == null) throw new ArgumentNullException("obj");
        lock (_db) _db.Remove(obj);
    }

    [Conditional("DEBUG")]
    public static void Finalizer(T obj)
    {
        if (obj == null) throw new ArgumentNullException("obj");
        InvokedFinalizerException exc;
        lock (_db)
        {
            if (_db.TryGetValue(obj, out exc)) _db.Remove(obj);
        }
        if (exc != null)
        {
            Debug.Fail(exc.ToString());
            // throw exc;
        }
    }
    ... // private class ObjectEqualityWeakReference
}

FinalizationDebugger<T> を使用する

public class DisposableClass : IDisposable
{
    private SomeOtherDisposableClass _data = ...;

    public DisposableClass()
    {
        ...
        FinalizationDebugger<DisposableClass>.Constructor(this);
    }

    public void Dispose()
    {
        Dispose(true);
        GC.SuppressFinalize(this);
    }

    protected virtual void Dispose(bool disposing)
    {
        if (disposing)
        {
            FinalizationDebugger<DisposableClass>.Dispose(this);
            _data.Dispose();
            ...
        }
    }

    ~DisposableClass()
    {
        FinalizationDebugger<DisposableClass>.Finalizer(this);
        ...
    }
}

このクラスの実装については多少の説明が必要です。FinalizationDebugger には静的 Dictionary が含まれており、この静的 Dictionary で、関連オブジェクトがいつどのように構築されたかに関するすべての情報が含まれているカスタムの例外クラスにオブジェクトをマップします。オブジェクトが構築され、Constructor メソッドを呼び出してオブジェクト自体が引数として渡されると、オブジェクトは例外のインスタンスと共にこのテーブルに挿入されます。Dispose メソッドが呼び出されると、オブジェクトおよび関連付けられている例外がテーブルから削除されます。Finalizer メソッドが呼び出されると、オブジェクトが存在するかどうかを確認するためテーブルに対してクエリが実行され、存在する場合は、関連付けられている例外が取得されてアサートに使用されます (必要に応じてスローすることもできます)。もちろん、この実装には複雑な点もいくつかあります。

第 1 に、これらは静的メソッドであり、複数のスレッドから同時に呼び出される可能性があるので、共有データへのアクセスを保護する必要があります。これを行うため、モニタを使用して基になる Dictionary へのすべてのアクセスを保護します。

第 2 の問題はより興味深いものです。Dictionary は、FinalizationDebugger クラスの静的メンバであるため、GC ルートになります。そのため、この Dictionary に格納されているすべてのオブジェクトはいつでもアクセス可能であるので、ガベージ コレクタによる収集の対象になることはありません。破棄可能なクラスのインスタンスをこの Dictionary に単に格納したとすると、これらのインスタンスはガベージ コレクションで一切使用できないのでファイナライズされず、このシステム全体が機能しなくなります (大量のメモリ リークが発生するのは言うまでもありません)。これを解決するには、弱い参照が必要です。

System.WeakReference クラスは、ガベージ コレクタと System.GCHandle が提供する機能を利用します。オブジェクトの WeakReference をインスタンス化すると、内部的に WeakReference は弱い GCHandle (GCHandleType.Weak または GCHandleType.WeakTrackResurrection) をそのオブジェクトに割り当てます。この場合、オブジェクトへの参照を格納するのではなく、単にこの GCHandle を格納します。弱い GCHandle はオブジェクトの追跡に使用されますが、そのオブジェクトは収集の対象のままです (オブジェクトが収集されると、GCHandle の内容はゼロに設定されます)。したがって、WeakReference を使用すると基になるオブジェクトにアクセスできますが、オブジェクトへの強い参照は保持しないので、ガベージ コレクタが実行されオブジェクトが強い参照がなければ収集可能である場合は、オブジェクトが収集され、それ以降そのオブジェクトにアクセスしようとすると WeakReference は null を返します。このように、WeakReference によって静的 Dictionary の問題が解決されます。実際のオブジェクトの代わりに WeakReference を格納することで、オブジェクトが収集可能なままになります。

ここで新たな問題が生じます。Dictionary では、オブジェクトのハッシュ コードとその Equals メソッドを使用して、オブジェクトを Dictionary のどこに格納するか、およびオブジェクトが Dictionary 内に既に存在しているかどうかを判断します (参照などのため)。一方、WeakReference 参照型は、GetHashCode と Equals のどちらもオーバーライドしません。その等価性のセマンティクスは既定で System.Object の GetHashCode または Equals に設定されるので、比較している 2 つの WeakReference インスタンスがまったく同じインスタンスであるかどうかがチェックされます。その結果、次のようなコードでは "False" が 2 度出力されます。

object a = new object();
Dictionary<object, bool> dict = new Dictionary<object, bool>();
dict.Add(new WeakReference(a), true);
Console.WriteLine(dict.ContainsKey(a));
Console.WriteLine(dict.ContainsKey(new WeakReference(a)));

これに対処するために、2 つの解決策を試みることができます。1 つは、作成した WeakReference を返すように FinalizationDebugger.Constructor メソッドを変更することです。DisposableClass がこの WeakReference を保持し、それ以降の FinalizationDebugger.Dispose および FinalizationDebugger.Finalizer の呼び出しでその WeakReference (それ自体ではなく) を渡すことができるようになります。

private WeakReference _weakRef;
...
_weakRef = FinalizationDebugger.Constructor(this);
...
FinalizationDebugger.Finalizer(_weakRef);

ただし、これによって別の問題が生じます。オブジェクトがファイナライズされる順序は定義されていません。したがって、あるファイナライズ可能なインスタンス (既にファイナライズされている可能性があるインスタンス) を別のファイナライズ可能なインスタンスのファイナライザで参照することは適切ではありません。WeakReference 自体がファイナライザを実装しているので、前のコード スニペットでは、破棄可能な型のファイナライザから既にファイナライズされている可能性がある WeakReference を使用しようとすることになります。

もう 1 つの解決策はここで実装するもので、問題をその根本から解決します。WeakReference での GetHashCode と Equals の実装です。これを行うため、図 7 に示す ObjectEqualityWeakReference というクラスを作成しました。このクラスは、WeakReference の Equals メソッドと GetHashCode メソッドをオーバーライドして必要なセマンティクスを提供します。ObjectEqualityWeakReference は、構築されると、指定されたオブジェクトのハッシュ コードをメンバ変数にキャッシュします。このキャッシュした値がオーバーライドした GetHashCode から返される値になります。これにより、ハッシュ コードが基になるオブジェクトに基づきます。オブジェクトが収集されても、ObjectEqualityWeakReference は同じハッシュ コード値を返し続けます。オブジェクトのハッシュ コードは変わるべきではなく、Dictionary では、そのテーブル内でオブジェクトを検索する最初の手段としてハッシュ コードを使用します。オブジェクトの追加後にそのオブジェクトのハッシュ コードが変わると、Dictionary でそのオブジェクトを見つけられない可能性が高くなります。

Figure 7 ObjectEqualityWeakReference (Close Figure 7)
private class ObjectEqualityWeakReference : WeakReference
{
    private int _hashCode;

    public ObjectEqualityWeakReference(object obj) :
        base(obj, true)
    {
        _hashCode = RuntimeHelpers.GetHashCode(obj);
    }

    public override bool Equals(object obj)
    {
        WeakReference other = obj as WeakReference;

        if (other == null) return ReferenceEquals(obj, this.Target);

        return ReferenceEquals(other, this) ||
               ReferenceEquals(other.Target, this.Target);
    }

    public override int GetHashCode() { return _hashCode; }
}
Figure 7 ObjectEqualityWeakReference (Close Figure 7)
private class ObjectEqualityWeakReference : WeakReference
{
    private int _hashCode;

    public ObjectEqualityWeakReference(object obj) :
        base(obj, true)
    {
        _hashCode = RuntimeHelpers.GetHashCode(obj);
    }

    public override bool Equals(object obj)
    {
        WeakReference other = obj as WeakReference;

        if (other == null) return ReferenceEquals(obj, this.Target);

        return ReferenceEquals(other, this) ||
               ReferenceEquals(other.Target, this.Target);
    }

    public override int GetHashCode() { return _hashCode; }
}

ObjectEqualityWeakReference は、Equals メソッドもオーバーライドします。Equals が動作するのは、ObjectEqualityWeakReference のインスタンスを同じ基になるオブジェクトを参照している別の WeakReference と比較する場合か、直接そのオブジェクトと比較する場合にしようとしました。そのため、Equals は、比較しているオブジェクトが別の WeakReference であるかどうかをまずチェックします。別である場合、Equals は、WeakReference インスタンスが同じであるかどうか、または基になるオブジェクトが同じであるかどうかを返します。比較しているオブジェクトが WeakReference でない場合は、Equals は、それが基になるオブジェクトと一致するかどうかを返します (Dictionary の TKey 型パラメータが ObjectEqualityWeakReference ではなく Object にバインドされているのは、基になるオブジェクトの型によるクエリではなく、ObjectEqualityWeakReference インスタンスの追加をサポートする必要があるためです)。

ObjectEqualityWeakReference に関する重要な注意点がもう 1 つあります。それは、そのコンストラクタです。

public ObjectEqualityWeakReference(object obj) : base(obj, true)

このコンストラクタは、1 つではなく 2 つのパラメータを受け取る WeakReference のコンストラクタにデリゲートします。2 番目のパラメータは、オブジェクトの追跡を停止するタイミングを WeakReference に対して指定するブール値です。この値が false の場合、オブジェクトが追跡されるのはファイナライズまでだけで、この値が true の場合は、オブジェクトはファイナライズ後まで追跡されます。ここでは、FinalizationDebugger.Finalize の呼び出しを経てオブジェクトが存続する必要があるため、true に設定します。このブール型パラメータは、作成される GCHandle の種類を指定するだけであることに注意してください。この値が false の場合は GCHandle.Weak が作成され、true の場合は GCHandle.WeakTrackResurrection が作成されます。名前付けは復活 (Resurrection) の概念に由来しており、オブジェクトはファイナライズ中も存続できます。オブジェクトのファイナライザによってオブジェクトへの新しいルート参照が作成されると (オブジェクトを静的フィールドのどこかに格納するなど)、そのオブジェクトは再びアクセス可能になるので収集されません。これを WeakReference と連携させるため、GCHandle でファイナライズ フェーズ後までオブジェクトが収集されないようにする必要があります。このことから、既に説明した動作に "WeakTrackResurrection" という名前を付けています。

これで、適切に機能する静的クラスの解決策は完了です。ただし、他の方法と同様に欠点があります。1 つには、保護する必要がある静的 Dictionary のため、この方法ではロックが必要となるので、多数のオブジェクトが構築されて破棄されている場合に競合が発生する可能性があります。これは、いくつかの手段である程度軽減されます。まず、ロックされた領域で実行される作業を最小限に抑えます。ここでは単に、Dictionary.Add 呼び出しを Constructor で、Dictionary.Remove 呼び出しを Dispose で、Dictionary.TryGetValue 呼び出しと Dictionary.Remove 呼び出しを Finalizer でラップしています。次に、追跡されているオブジェクトの型に基づいて FinalizationDebugger をジェネリック クラスにしました。これは、実際には、使用している型ごとに新しい FinalizationDebugger 型全体を作成するので (個別のロックおよびディクショナリを使用)、同じ型に基づく呼び出しだけが競合の原因になります。

この方法のもう 1 つの欠点は、ファイナライザでのロックが推奨される操作ではないことです。何らかの理由でファイナライザが適切な時間内にロックを取得できない場合、CLR によってファイナライザが単に停止されることがあります。ただし、現在のコードであればこの動作はまず起こりません。

この方法で考えられる最大の欠点は、破棄可能なクラスで、FinalizationDebugger.Finalizer メソッドを呼び出せるようにするためにファイナライザを実装する必要があることです。Finalizer メソッドが呼び出されない限り、オブジェクトが正しく破棄されたかどうかを確認するためのチェックも実行されません。これは、クラスで既にファイナライザを実装している場合はたいした問題ではありませんが、実装してない場合は、この目的でファイナライザを追加する必要があります。

これらの方法のすべてには他のバリエーションがあり、ここでは検討しなかったその他の方法もあるはずです。これらの方法がそれぞれに抱えている各種の問題をすべて解決すると思われる新しい方法があれば、是非お聞かせください。それまでは、これらの方法のいずれかが、破棄可能な型を使用している開発者がオブジェクトを正しく破棄していないケースを特定できるようにするうえで役立つでしょう。

ご意見やご質問は、Stephen netqa@microsoft.com.

まで英語でお送りください。
Stephen Toub は、マイクロソフトの並列コンピューティング プラットフォーム チームのシニア プログラム マネージャです。MSDN Magazine の寄稿編集者でもあります。


{ End Bracket }: 開発者向けでもある WPF

{ End Bracket }
開発者向けでもある WPF
Kevin Moore

Windows® Presentation Foundation (WPF) に関する 興味深い構築を最初の数か月間見守ることができたのは非常に楽しい経験でした。Expression Blend™ の導入により、WPF は、デザイナを考慮して構築されたマイクロソフトの最初のユーザー エクスペリエンス プラットフォームとして確立しました。このデザイナの地位向上は XAML (オブジェクトを記述するための XML ベース モデル) により実現されました。WPF の場合、XAML でコントロール、スタイル、およびアニメーションを記述します。XML ベースの XAML は、Windows フォーム アプリケーションに対して生成される C# または Visual Basic® のコードよりもはるかに使いやすいため、デザイナはプログラミングの詳細ではなくアプリケーションの外観や対話に集中できます。

しかし、この XAML ベースの機能については中傷者がいないわけではありません。私は、「単にコードを記述したい」と嘆く複数の不満そうな開発者とチャットしました。これはおそらく未知なものへの不安や知識のなさから湧き出た不満であると思います。開発の途中まで WPF に参加していた者として、XAML に対する少々の疑念があることはわかっています。「オブジェクトをインスタンス化する別の方法だって?」と思う人は、私を信じてください。XAML は恐れるのものではなく、受け入れることができるものです。アプリケーションのレイアウトを理解する必要がある場合は、波かっこやセミコロンのページではなく、入れ子になった要素や属性のページを理解する方がはるかに簡単です。また、XAML はさまざまなツールで使用できるように設計されているので、移植性もあります。

それでは、開発者はどこに関係するというのでしょうか。Visual Basic や C# のファンは、ボタンのイベント ハンドラを記述する余地があるというのでしょうか。心配ありません。ラスベガスで開催された MIX 2007 カンファレンスで、私は WPF アプリケーションにおいて開発者が実際に果たすことができる重要な 3 つの役割と、XAML コンテンツで優れた "Better Together" (組み合わせによる相乗効果) 体験を実感できる点を説明しました。これらの役割では、開発者を、配管工、ウィジェット作成者、および開拓者として表します。

配管工というのは、多くの開発者が UI には興味がなく、強力なデータ層を構築することでデザイナに可能性を与えることを好むという考え方によるものです。たとえば、一連の株を追跡するアプリケーションを構築するとします。各株オブジェクトをパブリック プロパティで定義することで、各株を視覚化するデータ テンプレートを簡単に定義できます。オブジェクトに INotifyPropertyChange を実装すると、視覚化はデータの変更に伴って透過的に更新されます。WPF コマンド インフラストラクチャを利用し、ICommand インターフェイスを使って、ビジネス オブジェクトの動作への宣言的なアクセスを可能にすることができます。たとえば、単に Refresh メソッドを公開するだけでなく、RefreshComannd を公開します。これは、イベント ハンドラを記述しなくてもボタンまたはメニューに接続できます。コマンドを実行すると、アクションが実行できない場合に関連する UI が無効になるため、デザイン ツールを使用するユーザーは、ビジネス ロジックを分離しながらも作成プロセスにおいて最大限の柔軟性を活用できます。

Expression Blend と適切に定義されたデータ層を使用して作業する経験豊富なデザイナは大きな役割を果たします。付属のコントロールの多くは、スタイルおよびコントロール テンプレートで調整できます。バインドやデータ テンプレートを使用することで、ビジネス オブジェクトのリッチな視覚化を実現できます。しかし結局は、デザイナは WPF に付属のコントロールと要素 (ウィジェット) に制限されます。一方、開発者は、特定のシナリオを対象とした新しいカスタムのウィジェットを定義することで、XAML 定義のアプリケーションの有効性を大きく向上させることができます。任意の数の要素を円形に配置する必要があるデザインがあるとします。この場合、描画 API を使用して要素を構築するのが一般的な方法でしょう。この方法は制約されたシナリオでは完璧ですが、それだけに制限されます。子要素の外観を変更する必要がある場合は、コードを変更しなければなりません。より適切な方法として、Panel をサブスクラス化する方法があります。これで、任意のビジュアル要素で使用できる要素ができたので、デザイン ツールで ItemsControl を使用して任意のデータ項目を表示できます。これは単なる一例です。WPF でデザインの問題に対処する方法は数多くあります。さまざまなコンポーネントがどのように連携して動作するかを理解すると、開発者は柔軟性を最大限に活かしてウィジェットを作成できます。

"配管工" が邪魔にならないようにコーディングし、"ウィジェット作成者" が他のコンポーネントと連携するようにコーディングしているとしたら、"開拓者" は話題をさらいます。WPF の中心には、強力な 2D/3D グラフィックス エンジンがあります。このエンジンの重要な部分は、XAML ベース ツールのコントロールやその他のフレームワーク機能を通じて公開されていますが、賢いプログラマが見つけてくれるのを待っている機能がまだ多数残されています。2D と 3D 間の相互運用性を利用して斬新なデータの視覚化を構築する場合でも、カスタムのアニメーションを実行して基本的な遷移を提供する場合でも、コンパイラを手中に収めている開発者は、WPF の最大のパワーと柔軟性を活用できます。優れた洞察力および一連のベスト プラクティスと組み合わせることで、デザイナと開発者が WPF で実現できることは数限りなくあります。

Kevin Moore は、マイクロソフトの Windows Presentation Foundation チームのプログラム マネージャであり、データとコントロールの処理を行っています。彼のブログは、work.j832.com で公開されています。