COMはハマる - Documents

何が問題か?

COM使った外部モジュールなんかが突然Segmentation Faultなどで落っこちたりする.

これ,一見外部モジュール内単独で発生する例外のため外部モジュール単体問題としか判断できず,原因から追うことは難しい.

しかし,こんなときはいっぺんCOMオブジェクトの使い方に依拠する問題じゃないか?と疑ってみてもいい.

COMについておさらい

Component Object Model.オブジェクト間通信(ORBA)の実装パターンのひとつ..だと思ってるが,手軽に扱える外部モジュールという印象がある.実際は並列処理安全性の担保とパフォーマンスをユーザが自身で設計に考慮する必要性を求められる.これはCOMの初期化APIに与えるオプションから明らかにできる.

CoInitializeEx(LPVOID, DWORD);

第二引数のフラグは,COINIT_APARTMENTTHREADED, COINIT_MULTITHREADEDのどちらかを指定する.

(CoInitialize(LPVOID)はCOINIT_APARTMENTTHREADEDが指定されたとみなされる)

ここでアパートメントなどというCOM独自概念が登場する.これは概念として分離されたスレッドと関連リソースのコンテキストといっていい.スレッドとはコードとスタックのことであるが,それプラス独自のメモリ空間(おそらくスレッドローカルストレージによって実装されている)と,ORBAを行うための共通メッセージングスタブなどが含まれたものを抽象化した定義である.

ただし,プロセスのように(OSとCPUによって)完全に分離された空間ではなく,特に保護されてるわけでもない.同一プロセス内であれば,あるアパートメントから別のアパートメントのメモリアクセスは,*表面上は* 何の問題もなくできる.アパートメントは概念モデルであり,Sandboxのように物理的に実現されているわけではない.この事実が,適切な使い方をしなかったときに発生する問題を深刻化する.

COINIT_APARTMENTTHREADED

ひとつのアパートメントを生成し,自身のスレッドをそこに属させることを指示する.このアパートメントをSTAと呼ぶ.STAのスレッドは常にひとつだけである

COINIT_MULTITHREADED

自身のスレッドはどのSTAにも属さないことを宣言する.(内部的にはMTAというアパートメントに属するようになる.MTAとはプロセスにひとつ存在する複数スレッドが属する特殊なアパートメントである)

要はCOMオブジェクトに対する並列処理安全性の担保を丸投げするか,自分でやるか,その違いである.

問題は,宣言した以上,丸投げしたならば常に丸投げせねばならないし,自分でやるというなら常に自分でやらねばならないというところにある.

なぜなら,COMオブジェクトのメソッドは実際の呼び出し対象となる外部モジュール本体ではなくORBAのスタブであり,Win32APIが提供するメッセージング機構をインフラとした通信によって実現されているRemote Procedure Callだからである(という可能性がある.実際はブラックボックス).つまりCOMオブジェクトはそれが作られたアパートメントに暗黙的に依存している.

よって,あるアパートメントで作成されたCOMオブジェクトのメソッドが別のアパートメントで呼び出されることは想定されていない.そうした場合の結果は不定である.

また,Windowを扱うスレッド(メッセージループをもつスレッド)は,Windowに対するメッセージの並列処理安全性を担保する必要があり,必然的にシングルスレッドであるSTA,COINIT_APARTMENTTHREADEDによって作成される空間に属さなければならない.

開発環境によっちゃあ,メインスレッドは既にSTAに属してる(CoInitializeEx(nil, COINIT_APARTMENTTHREADED)が実行されている)かも.COINIT_MULTITHREADEDを指定してみてRPC_E_CHANGED_MODEが返ったり,COINIT_APARTMENTTHREADEDを指定してみてS_FALSEが返るなら間違いない.Delphiなんかはそうなってる.

また,当然スレッド毎に実行されるようにしなければならない.COMアクセスとCoInitializeが分離するような作りになっていてはいけない.

こんな感じで,必要性から出発したものが新たな必要性を次々と生み,結果的に解りにくい抽象概念とわけのわからん制限,注意点,暗黙のルールを生み出し,非常に混沌としている.マジわかりにくい.

どう対策する?

本題である.

  1. メイン以外で作成されたユーザスレッドは,それがWindowを扱うのなら,必ずCOINIT_APARTMENTTHREADEDを使え

  2. COINIT_APARTMENTTHREADEDを使ったスレッド間でCOMオブジェクトを使い回してはいけない

  3. COINIT_APARTMENTTHREADEDを使ったスレッドでは,COMオブジェクトは常にそのスレッドで生成されたものを使え

  4. COINIT_MULTITHREADEDを使ったスレッド間ならCOMオブジェクトを使い回してもいい.でも排他は自己責任

  5. 上記を守らなくとも問題はないかもしれないし,あるかもしれない