記事のためおきってできるんですね・・。ちょっと使い方が微妙ですが・・・。
ちなみに、ファイル名からIStreamを開くだけならSHCreateStreamOnFileを使った方が早いです。
これはあくまで実装をやってみるときどうなるか、とCUnknownの使い方を示すものですので。
コンストラクタ
コンストラクタの実装は普通にファイルを開けばいいだけです。二つ宣言がありましたが、両方示しておきます。
CStream(IUnknown *lpOwner,HRESULT *lphr,LPCTSTR lpFileName) : CUnknown(NAME("CStream"),lpOwner), m_strFileName(lpFileName), m_hFile(INVALID_HANDLE_VALUE) { HANDLE hFile; hFile = ::CreateFile(lpFileName,GENERIC_READ,FILE_SHARE_READ,NULL,OPEN_EXISTING,FILE_ATTRIBUTE_NORMAL,NULL); if(hFile == INVALID_HANDLE_VALUE){ if(lphr != NULL){ *lphr = E_FAIL; } return; } m_hFile = hFile; if(lphr != NULL){ *lphr = S_OK; } } CStream(const std::string &strFileName,HANDLE hFile) : CUnknown(NAME("CStream"),NULL), m_strFileName(strFileName), m_hFile(hFile) { }
CUnknownを継承すると、CUnknownのコンストラクタに引数を2つ渡す必要があるいます。
第一引数 | 対象のCOMオブジェクトのデバッグ時の名前(NAMEマクロを使って指定する) |
第二引数 | 親となるべきIUnknownのオブジェクト(NULLなら自分自身を親とする) |
NAMEマクロはデバッグ時にはその文字コード環境に合わせた文字列となり、リリース時には空文字列となります。
また、ファイル名をとる普通のコンストラクタではHRESULTをどう渡すのかの一例が出ています。ファイルに開くことに失敗したときにはエラーコードE_FAILを渡して失敗した、と連絡しています。
ちなみに、CUnknownを継承すると、コンストラクタが終了したときの参照カウントは「0」で初期化されます。これは注意です。
デストラクタ
デストラクタです。このオブジェクトが解体されるときに呼び出されます。
このオブジェクトが解体されるときってどんなとき?というのはもちろんReleaseを呼び出して参照カウントが0になったときですが、ここで疑問が一つ。
じゃあ、このオブジェクトはどうやって「自身のメモリを解放する」のか(連動して、どうやってメモリを確保してコンストラクタを呼び出すのか)?
CUnknownを使うとこの処理は自動的にdeleteを使う、と決まります。なのでCStreamのメモリは常にnew~deleteを使用することになります。
IUnknownの処理を自前で実装すると、「その部分はメモリ解放を何で行うかによってやり方が変わる」ので注意してください。
もちろん、Releaseを呼び出して参照カウントが0になるんですから開かれているファイルを閉じればいいわけです。
CStream::~CStream() { if(m_hFile != INVALID_HANDLE_VALUE){ ::CloseHandle(m_hFile); m_hFile = INVALID_HANDLE_VALUE; } }
ファイルが開かれていない(コンストラクタでE_FAILを返したパターン)もちゃんと考慮に入れないとだめですね。
QueryInterfaceの実装
CUnknownを継承するとQueryInterfaceの機能を持たせるときはNonDelegatingQueryInterfaceという関数で記述することになります。
説明なしですが、とりあえず見てみましょう。
STDMETHODIMP CStream::NonDelegatingQueryInterface(REFIID refiid,void **lplpInterface) { if(IsEqualGUID(refiid,IID_IStream)) return GetInterface(static_cast<IStream *>(this),lplpInterface); return CUnknown::NonDelegatingQueryInterface(refiid,lplpInterface); }
COMオブジェクトにはすべてInterfaceIDがあり、それを使ってインターフェイスを指定します。今の場合はIStreamなので、それはIID_IStreamという定数値となります。
ここでの処理は
- IStreamを要求している(IsEqualGUIDによる判定) => GetInterface関数を使ってインターフェイスを返す
- それ以外を要求している => CUnknown側(つまり自分が継承している上位側)のNonDelegatingQueryInterfaceに任せる
です。一応説明しておきますが、IsEqualGUIDの処理は二つのGUIDが同じGUIDかどうを判定する関数(あるいはマクロ)です。単純に
if(refiid == IID_IStream) return GetInterface(static_cast<IStream *>(this),lplpInterface);
と書いても間違いではありません(構造体同士の比較演算子は構造体のメモリ自体の比較に変換される)が、こちらを使うのが本来は正しいです。
GetInterfaceはインターフェイスを返すときに対象のインターフェイスの参照カウントを一つあげて(つまり共有数を増やして)返す、という機能を持つ関数で、
これはQueryInterfaceではよく使う関数です。GetInterfaceはDirectShowを開発するときしか使えない関数ですので自分で使いたいときは同機能の関数を実装してください。
で、ここで処理できないものはCUnknown側の処理に任せることで解決しています。実はここがこの関数を仮想継承をしない理由で、つまり、CUnknown(系)を継承したときのQueryInterfaceの動作は
- IUnknownのQueryInterfaceの呼び出し
- 呼び出しがCStreamのNonDelegatingQueryInterfaceの呼び出しとなって呼び出される
- 処理されなければ上位のCUnknownのNonDelegatingQueryInterfaceの呼び出しになる
という風に継承の子供から親方向への呼び出しの連鎖を作ることでこの処理を正しく処理するようにできています。(実際にはこれは仮想関数なのですが、説明がわかりやすいようにと)
実際に開いて閉じてみる
今までの説明からCStreamを使用するときのやり方も決まります。つまり、以下のようなコードになるわけです。
IStream *lpStream; CStream *lpFileStream; lpFileStream = new CStream(NULL,&hr,TEXT("test.txt")); if(FAILED(hr)){ delete lpFileStream; return FALSE; } lpFileStream->AddRef(); hr = lpFileStream->QueryInterface(IID_IStream,(void **)&lpStream); lpFileStream->Release(); lpFileStream = NULL; if(FAILED(hr)) return FALSE; ... lpStream->Release(); lpStream = NULL;
開くときはnewで作ればいいわけですが、開くのに失敗したときはReleaseではなく即座にdeleteを使ってC++のクラスとしてメモリを解放してしまうのが正しいです。
開くことに成功すれば参照カウントを1にしてやって、以降はIUnknownのオブジェクト的な扱いを許されることになります。
CStreamからIStreamを取得するには、継承しているので単純にアップキャストしてもいいといえばいいですが、COMの礼儀に従ってQueryInterfaceでIStreamを取得します。
取得に成功すればlpFileStreamはいらなくなるのでReleaseして使用しないようにします。
ちなみに、参照カウントの動きはこんな感じになります。
動作 | 参照カウント | 説明 |
---|---|---|
new CStreamが完了する | 0 | CStreamのコンストラクタが呼び出されてファイルが開かれる |
lpFileStream->AddRefが完了する | 1 | lpFileStreamが参照カウント的に有効になる |
lpFileStream->QueryInterfaceが完了する | 2 | lpFileStream,lpStreamに同じオブジェクトを指す |
lpFileStream->Release、lpFileStream=NULLが完了する | 1 | lpStream側に使用権が移動 |
lpStream->Releaseが完了する | 0 | CStreamのデストラクタが呼び出されてファイルが閉じられる |
次回は残りの関数を実装してみます。今回のネタがCOMやDirectShowでのベースとなる動きですのでよく覚えましょう。