デバイスドライバを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++をとりあえず使いたい」という所とずれているように思う
#(ユーザーモードとカーネルモードで同じコードが書けるようにする為のライブラリ)。
#そのため、今までのドライバ開発の流儀からは「相当」離れなければならない事を覚悟する
#必要がある。まだまだ実験段階のプロジェクトなので、製品開発に使うのは難しいだろう。