COMのアパートメント (4) スレッド親和性

前回までの内容で、COMが裏で何をしているのか、少し見えたような気がすると思う。
ここで重要な「嘘」を公表しなければならない(大げさか)。

今まで、ファサードが存在する理由としてスレッド同期を挙げたのだが、そのこと自体は間違っていないのだが、同期の対象が違っていた。
コンポーネントの「単一のインスタンス」について同期を行うために、ファサードを挿入すべきか否かという話だったのだが、実はそうではない。

本当のCOMでは、同期の対象は「ウインドウ」である(ほぼ)。
Win32 APIの生の呼び出しで、一からウインドウを生成するコードを書いたことがあれば、これは自明である。ウインドウにまつわるAPIの大半は、対象とするスレッドを限定する。

たとえば、Visual C++で、新しくWin32のプロジェクトを開始してみてほしい。ひな形として生成されるCのコード(微妙にC++だが)は、Win32 APIの直接呼出しの、懐かしい、目を背けたくなるコードだ。そのコードには、CreateThreadや、beginthreadexのような、スレッド生成のAPI呼び出しは存在しない。つまり、このコードは「シングルスレッド」で実現されている。

実行すると、単一の味気ないウインドウが表示される。一通りの操作が可能である。ウインドウが生成されると、「ウインドウハンドル」が返される。ウインドウハンドルの値は不透明値なのだが、user32ライブラリ内で非公開の構造体を指しており、この構造体はウインドウを生成したスレッドと結びついている(スレッドハンドルを保持しているのか、スレッドローカルストレージに関連付いているのか、詳細はわからない)。

そして、ウインドウメッセージは、例のメッセージ転送ループ(GetMessage→DispatchMessage)によって、これらのウインドウに転送されるのだが、このループはウインドウが関連付けられているスレッドで実行される(というよりも実行するようにコードを書く)。
プロジェクトテンプレートでは単一のメインスレッドを使うので、これらが全て同じスレッドで実行されるわけだ。

さて、たとえばウインドウにボタンがあったとして、ボタンクリックに反応するコードを書いたとしよう。「シングルスレッド」なので、ボタンクリックの処理中に別のメッセージが発生することはない。
DispatchMessageからウインドウプロシージャが呼び出され、ボタンクリックのお手製コードが走っている。そのため、この処理が終わって、次にGetMessageが呼び出されるまでは、新しいメッセージが目に見える形では現れない。

したがって、マルチスレッド実行による、複数のメッセージの同時発生と実行を考慮する必要はないわけだ。
これは鶏と卵の関係だが、Win32のウインドウメッセージ処理にこういう制限があるから、競合処理は発生しないとも言えるし、競合処理が発生しないようにWin32 APIが設計されているともいえる。

この制限が時には苦痛となる。時間がかかる処理を行う場合だ。そのような処理をボタンクリックの処理で実装すると、ウインドウは他の処理が実行出来ない。ウインドウのサイズ変更・移動・ウインドウの描画まで含めて、「固まった」かのようになる。
仕方がないので、ここでワーカースレッドを生成して、長く時間のかかる処理を委譲したとする。これで、長い処理を並行して実行できるので、ウインドウの基本的な動作に支障は出なくなる(ボタンが2回クリックされないように無効化するなどの工夫は必要だが)。

しかし、このワーカースレッドの処理が完了したときに、それをどのようにウインドウに通知するかと考えた時に問題が発生する。
大半のウインドウAPIが、特定のスレッド(この場合はウインドウを生成したのがメインスレッドなので、メインスレッド)から呼び出されないと機能しない、あるいは誤動作を起こす。そこで、PostThreadMessageなどを使って、メッセージキューに結果となるメッセージを送信する。

メッセージキューはそのうちメインスレッドのGetMessageによって引き出され、晴れて「メインスレッド」でメッセージが処理される。この特別なメッセージの処理コードを書いておけば、ワーカースレッドの処理結果は間接的にメインスレッドの実行中に処理され、ウインドウに何らかの表示を行ったりすることが出来るようになる。

.NET Frameworkにおいても、System.Windows.Forms.Control.Invoke()などを使って、背景では同じことを行う。

乱暴な言い方をすれば、クリティカルセクションがウインドウに紐付いていて、ウインドウを基準に同期を行っていることに等しい。
分かっている人には当たり前のことではあるが、この制限をスレッド親和性のモデルとしたのが、COMのアパートメントというスレッド属性だ。何故か? それはCOMの構造を設計したときに、それがスレッディングにまつわるWindows独自の問題だったからだ(多分)。

何らかのアプリケーションのソースコードが手元にあったとする。このソースコードはすでに規模が大きく、マルチスレッドに対してどこまで安全であるかが分からない。注意深く設計されていれば、スレッドの導入にも頑健であるだろうが、今までにファサード導入で見てきたように、スレッド親和性を担保するには相応のコストが必要だ。おまけに、ウインドウの制御に関しても、どのウインドウがどのスレッドに紐付いていて、どう安全でどう危険であるかも明らかではない。アプリケーションによっては、GetMessageとDispatchMessageによるメッセージループが「複数」の別々のスレッドで実行されている可能性すらある。

また、同じ問題が(一般的なCOMではない)コンポーネントライブラリにも存在する。このような状況下では、あるコンポーネントがあるアプリケーションやコンポーネントと協調して動作させるなど、効率以前の問題で、悪夢としか言いようがない。サードパーティ製のライブラリを結合するとき、スレッド親和性についてほとんどの場合は無視(つまり親和性などなく、ウインドウとスレッドの安全は自分で担保が必要と仮定)するはずだ。

幸い、COMは「インターフェイス境界」が非常にはっきりしている。そのため、この境界を基準として、ウインドウに紐付いた暗黙のスレッドとの競合についても、背後で面倒を見ようとしたわけだ。

つづく


最新のCOM記事:

COMについて、ローカルディスカッションで大幅に拡充してまとめ上げた記事があるので、そちらもどうぞ:「ChalkTalk CLR – COMのすべて」

COMのアパートメント (3) スレッドの属性とコンポーネント

さてと、アパートメントという用語がどのあたりから来るのか匂わせておいたところで、2つの軸をどのように対処するのかを見ていく。

一つ目は、ファサードの有無の選択だ。
main関数で例を示したように、このままではクラスの呼び分けをする必要がある。
そこで、newする実装を隠ぺいすることを考える。たとえば、以下のような関数を用意する。

IAdd *CreateInstance(const bool threadSafe)
{
 IAdd *pInstance = new AddCalculator();

 // スレッドセーフなコンポーネントが必要なら
 if (threadSafe == true)
 {
  return new AddCalculatorFacade(pInstance);
 }
 else
 {
  return pInstance;
 }
}

呼び出す側は、スレッドセーフな実装が必要であればtrueを、そうでなければfalseを与えれば良い。しかし、これではクラスの型を指定するのが、boolの値にすり替わっただけだ。
そこで、いよいよコンポーネント自身にスレッド親和性の情報を付加する時がやってきた。

まず、コンポーネントの実装に、コンポーネント自身のスレッドセーフ属性を加える。これはコンポーネントのインスタンスが生成される前に参照する必要があるので、static関数とする。

class AddCalculator : public IAdd
 {
 public:
 // 途中省略
 static bool IsThreadSafe() throw()
 {
  // このコンポーネントはスレッドセーフではない
  return false;
 }
 };

class AddCalculator2 : public IAdd
 {
 public:
 // 途中省略
 static bool IsThreadSafe() throw()
 {
  // このコンポーネントはスレッドセーフだ
  return true;
 }
 };

IAdd *CreateInstance()
{
 // コンポーネントのスレッドセーフ属性を得る
 const bool componentIsThreadSafe = AddCalculator::IsThreadSafe();
 // const bool componentIsThreadSafe = AddCalculator2::IsThreadSafe();
 IAdd *pInstance = new AddCalculator();

 // コンポーネントがスレッドセーフでなければ
 if (componentIsThreadSafe == false)
 {
  // ファサードを挟んでスレッドセーフにする
  return new AddCalculatorFacade(pInstance);
 }
 else
 {
  return pInstance;
 }
}

まだ納得いかないだろう。特にスレッドセーフ属性を取得するくだりは、コンポーネントの型が分かっているのだから、意味がないように見えるかもしれない。今はテンプレートを使ってお茶を濁しておく。テンプレート経由だが、コンポーネントが自身でスレッドセーフ属性を提供している事がわかると思う。

template <typename T>  IAdd *CreateInstance()
{
 // コンポーネントのスレッドセーフ属性を得る
 const bool componentIsThreadSafe = T::IsThreadSafe();
 IAdd *pInstance = new T();

 // コンポーネントがスレッドセーフでなければ
 if (componentIsThreadSafe == false)
 {
  // ファサードを挟んでスレッドセーフにする
  return new AddCalculatorFacade(pInstance);
 }
 else
 {
  return pInstance;
 }
}

int main()
{
 // まだ実質的な変化はない...
 IAdd *pInstance = CreateInstance<AddCalculator>();
 // IAdd *pInstance = CreateInstance<AddCalculator2>();

 // 使う...
 delete pInstance;
 return 0;
}

上のコードに意味が見いだせないのは当然で、今までやって来た事の形を変えただけだからだ。「ついに」、もう半分の肝心な部分を付け足す。それは、現在のスレッドの指標を加味することだ。

// スレッドローカルストレージ(スレッド毎に保存される変数)
static __declspec(thread) bool g_MultiThreadBound = false;

// 現在のスレッドにスレッド属性を設定する
void SetThreadBound(const bool isMultiThreaded) throw()
{
 g_MultiThreadBound = isMultiThreaded;
}

template <typename T>  IAdd *CreateInstance()
{
 // コンポーネントのスレッドセーフ属性を得る
 const bool componentIsThreadSafe = T::IsThreadSafe();
 IAdd *pInstance = new T();

 // 現在のスレッドがマルチスレッドで使用する前提でかつ、
 // コンポーネントがスレッドセーフでなければ
 if ((g_MultiThreadBound == true) && (componentIsThreadSafe == false))
 {
  // ファサードを挟んでスレッドセーフにする
  return new AddCalculatorFacade(pInstance);
 }
 // コンポーネントがスレッドセーフなら、現在のスレッドがどのような属性でも問題ない。
 // コンポーネントがスレッドセーフでなくても、現在のスレッドがシングルスレッドでのみ使用する属性なら問題ない。
 else
 {
  return pInstance;
 }
}

さて、これで、コンポーネントを使用する側にとっては、スレッドの状態を表明さえすれば、そのコンポーネントがスレッドセーフだろうが、そうでなかろうが、安全に使用できるようになった。使用者はコンポーネントがスレッドに対して安全であるかどうかを考えなくてもよくなったという事だ。
シングルスレッドで使用するなら、

int main()
{
 // シングルスレッドでのみ使用する場合
 SetThreadBound(false);

 // どちらの実装を使ったとしても、最適なインスタンスが提供される。
 IAdd *pInstance = CreateInstance<AddCalculator>();
 // IAdd *pInstance = CreateInstance<AddCalculator2>();

 // 使う...
 delete pInstance;
 return 0;
}

マルチスレッドで使用するなら、

// スレッドのエントリポイント
int ThreadEntryPoint()
{
 // マルチスレッドで使用する場合
 SetThreadBound(true);

 // どちらの実装を使ったとしても、最適なインスタンスが提供される。
 IAdd *pInstance = CreateInstance<AddCalculator>();
 // IAdd *pInstance = CreateInstance<AddCalculator2>();

 // 使う...
 // (場合によっては別のスレッドにポインタを渡して使うかも)
 delete pInstance;
 return 0;
}

なんとなく、普段COMでやっていることに近づいてきたのが分かるだろうか。
つづく。


最新のCOM記事:

COMについて、ローカルディスカッションで大幅に拡充してまとめ上げた記事があるので、そちらもどうぞ:「ChalkTalk CLR – COMのすべて」

COMのアパートメント (2) コンポーネントファサード

もうあとちょっと導入を。

前回のファサードクラスによる分離を進めてみる。
このファサードクラスは、AddCalculatorクラスを内包してしまっているため、インスタンスを外部から与えられるようにすると、柔軟性が増す。

class AddCalculatorFacade : IAdd
 {
 private:
  AddCalculator *pInner_;
  CRITICAL_SECTION cs_;

 public:
  AddCalculatorFacade(AddCalculator *pInner) throw()
   : pInner_(pInner)
  {
   ::InitializeCriticalSection(&cs_);
  }

 ~AddCalculatorFacade() throw()
  {
   delete pInner_;
   ::DeleteCrirticalSection(&cs_);
  }

 LONGLONG Add(const LONGLONG adder) throw()
  {
   ::EnterCriticalSection(&cs_);
   const LONGLONG result = pInner_->Add(adder);
   ::LeaveCriticalSection(&cs_);
   return result;
  }
 };

これならば、コンストラクタでインスタンスを指定することが出来る
(更にAddCalculator *を、IAdd *にすれば、もっと汎用的になる)。しつこいが、念のため例を示す。

int main(int argc, const char *argv[])
{
  IAdd *pAddInner = new AddCalculator();

  // ここだけで選択(ファサードを挿入するかどうか)
  IAdd *pAdd = pAddInner;
  //IAdd *pAdd = new AddCalculatorFacade(pAddInner);

  const LONGLONG result = pAdd->Add(12345);
  delete pAdd;

  printf("Result=%I64dn", result);
  return 0;
}

で、いよいよスレッド親和性の話になるのだが、以下の事が重要なポイントだ。

  • あるコンポーネント(クラス)が、スレッドに対して安全かどうかを表明する。
  • コンポーネントにアクセスしようとしているスレッドが、「安全なスレッド」かどうかを判別する。

ということが出来れば、コンポーネント毎にスレッドの安全性を「ファサード」を使って自動的に担保できるようになる。

あるコンポーネントは、最初のAddCalculatorのように、スレッドに対する安全性が無いとする。それを、単一のスレッドでしか実行しない場合は、ファサードが挿入されない。それによって、効率よくアクセスすることが出来る。

同じコンポーネントで、複数のスレッドで同時にアクセスされることがわかっている場合の、「個々のスレッド」に対して、自動的にファサードを挿入したポインタを渡す。

これで、メソッドの呼び出しが同期化されて安全にアクセスできる。
初めからスレッドセーフなコンポーネントAddCalculator2を、単一のスレッドでしか実行しない場合は、ファサードは挿入されない。

同じコンポーネントで、複数のスレッドで同時にアクセスされることがわかっている場合の、「個々のスレッド」に対してファサードは挿入されない。
なぜなら、元からAddCalculator2はスレッドセーフだとわかっているからだ。

このように、スレッドとコンポーネントの両方に、何らかの「指標」やら「属性」があれば、スレッドセーフを自動的に担保することが出来る。
そして、スレッド側に付ける「指標」は、「アパートメント」という名前の属性だ。
つづく。


最新のCOM記事:

COMについて、ローカルディスカッションで大幅に拡充してまとめ上げた記事があるので、そちらもどうぞ:「ChalkTalk CLR – COMのすべて」

COMのアパートメント (1) スレッド同期の隠蔽

COMのわかりにくい概念のひとつに「アパートメント」がある。

COMのほとんどの概念は、抽象性を突き詰めていった結果として生まれているため、このアパートメントも解説がなければ非常に難しい。

DonBoxが書いた「Essential COM」に詳しく書いてあるが、それでも頭をひねって考える必要がある。
同じくEssential COMの第一章では、COMのインターフェイスという概念が生まれた背景について、非常にうまく説明している。ここでは、同じような方法でアパートメントを説明してみることにする。

#導入は実は少々怪しいところがあるが、この方法でアパートメントを解説している文書は見たことがないので、お役にたてれば幸い。

非常に初期の、C言語が広まる機運が高まりつつあるころ、C言語でプログラムを書いていた人は、次のような疑問に遭遇した。つまり、いかにしてprintfやstrcpyのような独立性の高い、再利用可能な関数を書いて、それをライブラリ化するかということだ。
COMのインターフェイスはそこが基礎になっているのだが、これは言ってみれば「バイナリレベルのライブラリ(コンポーネント・クラス)の結合」を自由に行うための技術と言っていい。そのために、インターフェイス定義でコンポーネントの「顔」を抽象化して、綺麗さっぱりコンポーネントを分離できるようにしたというわけだ。

アパートメントはこれを更に推し進めて、「スレッド親和性」を抽象化したものだ。で、スレッド親和性って何?という話になるのだが、これを以下で説明する。

ここに、非常に単純な、足し算を行うCOMコンポーネントがある。本当にIDLから説明すると面倒なので、C++のクラスでこれを模式してみる。

class AddCalculator
{
private:
 LONGLONG value_;

public:
 AddCalculator() throw()
   : value_(0)
  {
  }

 LONGLONG Add(const LONGLONG adder) throw()
  {
  value_ += adder;
  return value_;
 }
};

Add()を呼び出すことによって、引数の値を足し算して結果を取得する。結果は戻り値として得られる。
このクラスは一般的な使い方で十分機能する。しかし、それは「シングルスレッド」や「マルチスレッド中の特定のスレッド」で使用した場合のみだ。

たとえば、Add()をマルチスレッド中の複数の異なるスレッドから同時にアクセスした場合、計算や結果の戻り値が容易に破たんすることがわかるだろう。
32ビットプラットフォームならさらにひどい結果となる(だからLONGLONGで例を書いてみた)。

問題は、Add()のブロック内の足し算を行っている個所、そして結果をreturnで返している部分。ここが別々のスレッドで同時に実行されると、競合状態が発生する。
そのため、マルチスレッドでも問題なくコードを実行できるようにするためには、ここを「排他制御」で保護する必要がある。

一般的にはクリティカルセクションを使う。コードを書き直してみる。

class AddCalculator2
{
private:
 LONGLONG value_;
 CRITICAL_SECTION cs_;

public:
 AddCalculator2() throw()
  : value_(0)
 {
  ::InitializeCriticalSection(&cs_);
 }

 ~AddCalculator2() throw()
 {
  ::DeleteCrirticalSection(&cs_);
 }

 LONGLONG Add(const LONGLONG adder) throw()
 {
  ::EnterCriticalSection(&cs_);
  value_ += adder;
  const LONGLONG result = value_;
  ::LeaveCriticalSection(&cs_);
  return result;
 }
};

足し算の計算を行っている部分と、戻り値として返す値を確定させる部分(resultに代入する)をクリティカルセクションで保護した。

resultに代入しなおすのは、32ビット環境ではこの一行が2つかそれ以上のマシン語命令に置き換えられてしまい、そのコードを実行し終わるまでを保護する必要があるからだ。そして、たとえ64ビットであっても、return文の実行の直前にvalue_が書き換えられてしまうと、adderに指定した値以上の結果が返されてしまう可能性がある。それを保護するため、一旦resultに値を代入して、絶対に結果が変わらないようにするというわけだ。

さて、これでマルチスレッド環境で、複数のスレッドから同時にAddを呼び出しても問題なく動作するようになった。このAddCalculator2クラスは、すべてAddCalculatorクラスと置き換えることが出来る。つまり、マルチスレッド環境だけではなく、シングルスレッド環境でも問題ない。シングルスレッド環境ではクリティカルセクションで保護する意味はないが、保護していたとしても動作に影響はないからだ。

パフォーマンスの問題を除けば。

要するに、シングルスレッド環境で動かす場合、クリティカルセクションによる排他処理は完全に無駄になる。たとえば、上記のような単純な例でも1000万回呼び出されるとすれば、小さな排他制御のコードでさえ大きな無駄になる可能性がある。もちろん、コード全体からみて、この処理にどれだけの実行割合があるかによるのだが。

一つの方法としては、コンパイルオプションで、シングルスレッドの場合はAddCalculator、マルチスレッドの場合はAddCalculator2を使い分けるという方法がある。しかし、この方法には問題がある。

  • 常に二つの似て異なるクラスを保守しなければならない。
  • これらのクラスを使う側は、クラスのスレッドに関する安全性に気を配らなければならない。どちらのクラスがスレッドに対して安全なのか、そうではないのか、知っている必要がある。
  • 実行時に切り替える事は出来ない。

実はこの方法は、すでになじみのある方法だ。VC++ではシングルスレッドとマルチスレッドのライブラリを選択出来る(出来た。新しいVCではシングルスレッドライブラリは廃止されている。廃止された理由は、やはり「保守」の問題ではないだろうか)。

シングルスレッドなプログラムのコンパイルにマルチスレッドのライブラリを使っても実害はないが、速度は遅くなる。
さて、もう少し前向きな方法を考えてみる。同期処理をファサードで分離してみてはどうだろうか。AddCalculatorクラスのファサードAddCalculatorFacadeを作る。

class AddCalculatorFacade
{
private:
 AddCalculator inner_;
 CRITICAL_SECTION cs_;

public:
 AddCalculatorFacade() throw()
 {
  ::InitializeCriticalSection(&cs_);
 }

 ~AddCalculatorFacade() throw()
 {
  ::DeleteCrirticalSection(&cs_);
 }

 LONGLONG Add(const LONGLONG adder) throw()
 {
  ::EnterCriticalSection(&cs_);
  const LONGLONG result = inner_.Add(adder);
  ::LeaveCriticalSection(&cs_);
  return result;
 }
};

これで、計算の実態はAddCalculatorのまま、スレッドセーフなクラスとしてAddCalculatorFacadeを使えばよくなった。計算ロジックの保守は、AddCalculatorクラスだけを行えばよい。マルチスレッドの安全性が欲しい場合は、AddCalculatorFacadeを使い、シングルスレッドで安全であることがわかっていればAddCalculatorを使って「最速」を手に入れることが出来る。

このままでは、コンパイルオプションで切り分けるという部分は未解決だ。そこで、共通のインターフェイス「IAdd」を定義して分離してみる。

class IAdd
{
public:
 virtual ~IAdd() throw()
 {
 }
 virtual LONGLONG Add(const LONGLONG adder) throw() = 0;
};

class AddCalculator : public IAdd
{
private:
 LONGLONG value_;

public:
 AddCalculator() throw()
  : value_(0)
 {
 }

 LONGLONG Add(const LONGLONG adder) throw()
 {
  value_ += adder;
  return value_;
 }
};

class AddCalculatorFacade : IAdd
{
private:
 AddCalculator inner_;
 CRITICAL_SECTION cs_;
public:
 AddCalculatorFacade() throw()
 {
  ::InitializeCriticalSection(&cs_);
 }

 ~AddCalculatorFacade() throw()
 {
  ::DeleteCrirticalSection(&cs_);
 }

 LONGLONG Add(const LONGLONG adder) throw()
 {
  ::EnterCriticalSection(&cs_);
  const LONGLONG result = inner_.Add(adder);
  ::LeaveCriticalSection(&cs_);
  return result;
 }
};

これで、コードを使う実態は常にIAddインターフェイスを使ってアクセスし、クラスをnewするところだけで、AddCalculatorかAddCalculatorFacadeかの選択を行えばよい。

int main(int argc, const char *argv[])
{
 // ここだけで選択
 IAdd *pAdd = new AddCalculator();
 // IAdd *pAdd = new AddCalculatorFacade();
 const LONGLONG result = pAdd->Add(12345);
 delete pAdd;
 printf("Result=%I64dn", result);
 return 0;
}

さて、以上のようになっても、どちらのクラスを使用すべきか?という問題が残る。そして、シングルスレッドで使っていたクラスが、ある時から別のスレッドによってアクセスされるような場合に、この方法では対処出来ない。そのような場合、仕方がないから初めからマルチスレッド対応のAddCalculatorFacadeを使うという事になる。

これをどうやって解決するのかということの、頭をひねった結果が「アパートメント」という考え方だ。
つづく。


最新のCOM記事:

COMについて、ローカルディスカッションで大幅に拡充してまとめ上げた記事があるので、そちらもどうぞ:「ChalkTalk CLR – COMのすべて」

KeSetEventの第三引数

KeSetEventの第三引数について、「WDKプログラミング」のコラムにはTRUEを指定すべき理由は全くない、と言う事が書いてある。また、DDKの説明は混乱を招くとの事で、ここで詳しく動作を解説している。

が、この説明もカーネル同期の内部動作についてわかっている(あるいはなんとなくわかっている)ことが前提のような説明で、結局難解なままだ(翻訳が良くないのか?)。

ユーザーランドでの同期コードを書いていた人なら、イベントオブジェクトをセットして待つという動作を行うのに、SignalObjectAndWaitというAPIがある事を知っていると思う。カーネルAPIには対応するようなものが無いのだが、このKeSetEventの第三引数はまさにそれを実現するためにある。

KEVENT event;

KeInitializeEvent(&amp;event, SynchronizationEvent, FALSE);

KeSetEvent(&amp;event, IO_NO_INCREMENT, TRUE);

assert(KeGetCurrentIrql() == SYNCH_LEVEL);

LARGE_INTEGER timeout;
timeout.QuadPart = -10000000;
KeWaitForSingleObject(&amp;anotherEvent, Executive, KernelMode, FALSE, &amp;timeout);

KeSetEventの第三引数にTRUEを指定すると、APIから戻ってきたときにSYNCH_LEVELというかなり高いIRQLが維持される。これはそのままKeWaitForSingleObjectに引き継ぐことが可能で、この間同じCPUでのプリエンプションは絶対に発生しない。この組み合わせによって、SignalObjectAndWaitと同じことが実現できる。
なお、 SYNCH_LEVELで維持するのが妥当な部分はこの操作だけなので、当然待機しないでなにか別の作業を行うのはやめておいたほうが良い。実際のところ、こんな高いIRQLでは出来ることも限られてくるのだが…

また、はじめからDISPATCH_LEVELで実行すれば、同じことが実現できるような気がするのだが、DISPATCH_LEVELで時間を指定しての待機は「お手付き」なので、やはりこの方法しかない。
(多分TEBに、このモードを実行中でSYNCH_LEVELにしたというフラグか何かがあるのだろう)

いや、「ダサい」と思うよ。WindowsカーネルのAPIは、こういうダサさが至る所に漂っている。

#この本、深い部分を述べている点では良いのだが、どうも説明がわかりにくい。
#より古い「NT Driver」の方が理解しやすい。

デバイスドライバをC++で実装する

自宅のサーバがクラッシュしてから、しばらくブログを書かなかった(書けなかった)けど、また書く気になってきたのと、ちょっとゲストOSも多くなってきたので、自分でホストするのをやめて、wordpress.orgに間借りしようと思う。

書く気になってきたのは、Windowsドライバ開発ではメジャーなOSRのメーリングリストで度々話題にのぼる、「WDKはなぜC++を使えないのか」とか「C++で実装するのは悪いことだ」というネタがあるのだが、これについての思うことを書きたくなったからだ。

とはいっても、無名の設計者のしかも異端なたわごとなので、そういう考え方もあるという程度の情報であって、絶対的だとは思ってない。

基本的に、WDKでC++を使って書くことにはとても賛成だ。

  • Cと比較して強化された構文チェック、甘い書き方を縛り上げる道具、読みやすさ。
  • オブジェクト指向プログラミングがネイティブで行える(Cによる真似事ではなく)。
  • 例外が使える。加えてRAIIを併用することでリソース管理の苦痛をコンパイラ任せにできる。
  • テンプレートを使ってジェネリックなコードが書ける。
  • これらの道具を組み合わせることで、実装量を削減できる。

そんなわけで、WDKでコードを書くときには、迷わずC++を選択する。しかし、これを読んですぐに「よーし、俺もC++で」と思っても、以下のようなデメリットが挫折へと導く。

  • マイクロソフトがC++で書くことを推奨していない。
  • WDKがC++をサポートしていない。
  • 著名なMVPの方々が反対する。
  • C++で実装する場合に必要となる、プラスアルファの知識について公開されている情報が少ない。
  • WDKのビルドシステムがC++で書くことを拒絶している(未サポート)。

なお、英語だが、マイクロソフト自身が推奨していない理由が書かれた記事がある。
C++ for Kernel Mode Drivers: Pros and Cons

最初にこれらの反対について思うことのベースを書いておくならば、「いつまでもアセンブラからCへの過渡期と同じことをしてんじゃねーよ。足引っ張るな」ってことだ。あの時はまだ俺の技術レベルもPascal止まりでCってどうよ?的な所だったから、どちらがいいとか悪いとか判断はつかなかったが、今ならはっきりとCへの移行は正解だったといえる。

アセンブラからCへの過渡期に言われていた事を要約するなら:

  • アセンブラなら、実装について詳細まで把握できる。バグがあっても自分で見つけることが出来る。Cはコンパイラやランタイムが何をやっているのか理解できないし、理解したとしてもコード量が増えてきたらそれを詳細にチェックできないではないか。不具合の原因が調べられないようでは、それを標準とすることなど出来ない。
  • Cはアセンブラでは知らなければならない詳細な実装を簡単に隠蔽できる。すでに人がすべての実装を把握していくのは困難なのだから、機械(この場合はコンパイラ)に任せられる部分があれば、積極的に任せていくべきだ。Cでコード量が増えたときに、同じ実装をアセンブラで一体どれだけのコードが書けるのか?書いてそれは保守可能なのか?

というような主張で平行線だった気がする。その結果はどうかといえば、もはや今のソフトウェア資産はすべてをアセンブラで実装することは、マンパワー的にも、経済的にも、時間的にも許される状況ではない。確かに当時はコンパイラの質も悪く、不具合を起こす書き方というものがあり、それを回避する方法にまで注意を払う必要があった。今やそういう情報はすぐにコミュニティによって発見され、メーカーが公式に発表する前に話題にあがり、ナレッジとなっている。

そして、コンパイラ自体も十分洗練され、新しいコンパイラを実装する場合も初めからその質は十分に高い(ソフトウェア工学も発達・進化した)。

デバイスドライバをC++で書くべきではない、という主張には一定の理解できる理由はある。

  • リソースコントロールの問題。ドライバで使える資源(主にメモリ)は限られているので、リソースを暗に使用するC++は意識してコントロールしないとシステム全体の安定性を損なう。
  • 例外はコストが高い。使うべきではない。
  • テンプレートは生成されるコードが想像しにくい。バイトコードが肥大化する。

リソースコントロールについて言えば、「使わなければならないメモリを使うなら、安全に使える方法を採用したほうが良い」ということになる。これが、システムの能力が極めて脆弱な場合は仕方ないが、今やCPUは十分に速いしメモリを潤沢に存在する。カーネルモードではメモリの使用方法に制限がある場合があるが、ない場合(ユーザーランドとほぼ同等の事をしても問題ない)もある。これらをひっくるめて、ドライバだから一律やりにくい手法をとる必要がある、というのは、今までがそうだったからこれからも、のような不条理さを感じる。

例外のコストが高いことについては、二つの背景がある。一つは例外フレームの構築コード。例外のアンワインド処理(例外が発生したときに、例外処理を行うコードに遷移させるコードと、デストラクタを呼び出すコードを呼び出すコード)のために、関数の先頭に挿入されるコードが必ず実行される。これはわずかだが、コストが無いかといえばある。もう一つは実際に例外が発生したときに、上記アンワインド処理を実行するコードで、これはかなりのペナルティがある。スローされた型が何であるかどうか、例外ブロックやデストラクタの実行など、かなりいろいろな処理が走る。

このような例外の構造を知りたいなら、以下の記事が非常に優秀なので、参照するとよい。
Deep C++ / C と C++ での例外処理

JavaやC#といった言語でも言われていることだが、後者のアンワインド処理は非常に高くつくので、通常の処理の遷移で例外を使ってはならない事は、しつこく言われていることだ。たとえば深い再帰関数から脱出するためだけに、例外を使うなとか、例外は「例外的状況」が発生した場合にのみ使用せよとか。これと全く同じことをC++でも実践すればいいだけだ。

前者の例外フレーム構築コードについてはこう考える。もし、エラーの判定をすべて昔ながらの方法(つまりifやswitch)で実装したとする。C++ならローカルで確保したリソースの解放はRAIIを使って意図することなく自動的に行うことが出来る。しかし、複雑に分岐するifやswitchで、はたしてこれを全て漏れなく安全に実装できるだろうか?仮に出来たとして、それはどうやって検証するのか?検証するにしても、そのためのコストは?関数一つなら、一・二時間レビューしたら「いいかも?」ぐらい言えるかもしれないが。レビューという習慣がないチームや社風もある。ドライバをテスト駆動で開発することほど難しいものはない。こうした便利な道具をことごとく封じられている状態で、更に最も近くにあるコンパイラの恩恵をも、むざむざ捨てているようなものだ。

また、リソース解放のために新たにフラグを設け、確保したらフラグを立て、関数を抜けるときにそれをチェックして解放が必要なら解放するというコードが多い(Cで書けと言われれば、そうするしかないという、消極的な理由で。SEHの__try~__finallyを使う)。そういう「手で書けば実現」なんていうのは糞くらえだ。アセンブラなら全能だから全部アセンブラで、って言っているのと同じにしか聞こえない。人間はミスをする。だからこれを認め続けると、ドライバ開発で効率の上がる発展的なことは何も望めないということだ。そして、この件でいうなら、それを手で実装するのと、例外フレーム構築に必要な実行コストを天秤にかけても、まだ手で書いたほうがいいと言えるかどうか。俺の中では考えるまでもない。

例外について言えば、「例外的なことに例外を使う」という原則に従った場合、ドライバで例外がスローされることは「ない」。それは実際書いて実証した。Windowsの場合、NTSTATUSがSTATUS_SUCCESSやSTATUS_PENDINGぐらいしか、返す値は無いのだ。異常が起きたらそれはWindowsシステムの危機ぐらいの状態だ。本当に時々にしか起きない問題に対処するためのコードを書く。こういう実装でこそ、例外は真価を発揮する。

テンプレートの話題は、ばかばかしいレベルだ。C++でインライン展開と組み合わされることにより、関数呼び出しのコードが削減されて、驚きのコードが生成される。生成されるコードを比較したことがあるのかと言いたい(アセンブラレベルで)。もちろん、これはコンパイラがどれだけ「賢い」かによるところがある。だが、そうやってコンパイラが(全てのコードに対して)自動的に面倒を見てくれるのと、自分でちまちまと最適化出来る部分がないかどうかとか考えたり修正したりする(しかもコードを全部)のと、どちらがいいかと言う事だ。これもまた、アセンブラとCコンパイラの最適化の比較で、散々議論された。現代ではどうなっただろうか? 特別な事情がない限り、最適化を無効にした「リリースバージョン」をビルドすることはないだろう。

ただし、テンプレートについては難点がある。デバッグがやりにくい。これはドライバ開発ではなくてユーザーランドのVC++でのコードでも言われていたことだ。VC6時代にようやくSTLが実用レベルに達し、ATLで本格的にテンプレートを使用したコードが書けるようになり、今に至るまでに多くのデベロッパがこれに関与し続けた結果、Visual Studioのデバッガ能力はかなり改善された。テンプレートを多用したコードでも、優れたIDEのおかげでデバッグは非常に楽になっている。

だが、これもWDKになるとwindbgに頼るしかない。ここで、その貧相なデバッガの能力に幻滅する。同時に、WDKがC++を拒否している限り、ドライバ開発でC++を使って効率よく開発を進めるということが、恣意的に閉ざされているように思う。windbgは拡張DLLでコマンドを追加する拡張性があるため、windbgのプロフェッショナルにはとても重宝されているようだ。それはそれで結構なことだし否定はしないが、俺のようにユーザーランドからカーネルドライバまで一元で開発するデベロッパにとって、windbgは頭痛の種でしかない。カーネルレベルドライバの開発となると、途端に「ここは旧石器時代ですか?」みたいなのはどういう事だと。

仮にドライバ開発が.NET Frameworkのような手軽さになっても、ドライバ開発はドライバ開発で複雑な知識を要求することに変わりはない。だからそれでデベロッパの門戸が広がるなどとは思っていないが、それにしても20年前の開発を思い出すのはやはり健全とは思えない。

無いとは思うが、例えばLinux陣営がドライバ開発を非常に高い効率で開発できる「何か」を生み出したら、Windows陣営は冷静でいられるだろうか。Windowsもドライバ開発は簡単です!のような何かを生み出し、ドライバ開発とはこういうものだ、などと言っていた人も手のひらを返すように「開発はここまで簡単なんです」みたいなことを言い出すんじゃないか?

結局のところ、アセンブラとCの過渡期と同じことの繰り返しでしかないと思うのはこれが根拠だ。WDKがC++に対応していないがために、現在はC++を使うことが難しい状況にある。

  • C++例外が使えない。一部のサードパーティ製ツールを使うことで、C++例外を使用できるようになる。そうでないと、リンク時に例外のランタイムコードが無いためにエラーが発生する。
  • WDKの古いバージョンでは、extern “C”を使わないと、WDKのヘッダファイルでエラーが発生する。
  • カーネルモードドライバのインフラ(DRIVER_OBJECT, DEVICE_OBJECT, IRP, スタッカブルなドライバ構造)が、関数コールバックに依存しており、これがC++のオブジェクトと非常に相性が悪い。

C++例外については、Visual C++のランタイムライブラリから例外コードをうまく抜き出してこれをリンクすることで使用可能に出来る。もっとも、その他にも付随するコードをかなり移植ないし自分で書き足す必要があり、そこまで含めてC++を使用可能にして製品コードに適用するには、色んな面で敷居が高い(だが、俺はそれを選択した)。

ドライバの実装にC++を使うか、Cを使うか。こんなことはそれを使う側が決めることであって、マイクロソフトやドライバ開発のプロパガンタが強制することではない。メリット・デメリットをともに見せたうえで、ユーザーが判断することだ。.NETの世界がJavaと異なり複数の言語の選択性を持たせているのもそうだと思うのだが。

マイクロソフトを少し擁護するなら、ユーザーモードとカーネルモードのライブラリは異なるので、カーネルモードでC++を使用出来るようにするためのライブラリを提供することにはコストがかかる。だが、VC++の例外やRTTIに必要なコードのナレッジは非公開だ。CRTのソースコードには残念ながら「含まれていない」。この部分だけ、バイナリライブラリの提供なのだ。もし、CRTソースコードに例外サポートの為のソースコードが含まれていれば、もっと状況は明るかった。マイクロソフトがやらなくても、コミュニティが、ライブラリ生成のバッチファイルなど作ったかもしれない。そのような理由で、現状ではカーネルモードでC++例外などを使うためには「泥臭い」作業を乗り越えなければならない。

あとの問題は技術があればカバー出来るが、当然インフラを隠蔽するC++のライブラリがあればベターだ。KMDFは悪い方向に向かってしまった。あれのせいで余計にC++への移行が遅れるだろう。高度で堅牢でコンパイラの利点を最大限に生かすATLのようなライブラリが望ましかった。しかもテンプレートベースなら、動作の詳細をソースコードですぐに追いかけることも可能だ(Visual Studioのデバッガが使えれば、の話だが。インテリセンスとシンボル参照で次々とソースコードを追跡出来るというのが、何故windbgで出来ないのかという腹立たしさに直結する)。マイクロソフトがKMDFを設計したときに、きっと社内では様々なオプションが検討されたことだろう。せめてテンプレートベースのライブラリも検討され、あれはwindbgの存在によって却下されたと信じたいところだ。

結局、社内でも声が大きい者が勝つ。WDKの現状を見ると、レガシーに固執した人の声が大きいのだろうと感じる。彼らは彼らなりの論理があってC++を拒否している。そのこと自体に間違いはないが、開発効率のギャップは永遠に埋まりそうにないと思えてくる。

#なお、カーネルモードのC++ライブラリを作る「ontl」というコミュニティのプロジェクトがある。
#このライブラリはC++例外もサポートしているため、迷い子にとっては救世主のようだ。
#だが、ontlの目標は「C++をとりあえず使いたい」という所とずれているように思う
#(ユーザーモードとカーネルモードで同じコードが書けるようにする為のライブラリ)。
#そのため、今までのドライバ開発の流儀からは「相当」離れなければならない事を覚悟する
#必要がある。まだまだ実験段階のプロジェクトなので、製品開発に使うのは難しいだろう。