やっぱり番外を先にやります。IStreamというCOMオブジェクトを自分で実装することでどうやってIUnknownを持つオブジェクトを実装するか、をやってみたいと思います。
例にIStreamを選んだ理由は
- ファイルの読み書きで例になるのでWinAPIとの差やCOMの実装がどういうものかわかりやすい
- ほかのオブジェクトでIStreamを引数に読み書きを行うオブジェクトがかなりある(WindowsMediaを処理するIWMSyncReaderもその一種)
- というか自分のコードとして実装しているので説明しやすい
というものだったりします。
IStreamを実装する前に実装が必要となる純粋仮想関数をちゃんと調べておいて下さい。COMオブジェクトの実装はC++で純粋仮想を持つインターフェイスクラスを継承して
その部分をオーバーライドして実装することと同意義です。なので以降はかなりC++の知識が必要になります。
まずは宣言部です。さっさと宣言部を出しますね。今回はファイルの読み込み(書き込みはしない)の処理をIStreamに置き換える、というのをやってみます。
class CStream : public CUnknown, public IStream{ public: CStream(IUnknown *lpOwner,HRESULT *lphr,LPCTSTR lpFileName); //コンストラクタ(1) virtual ~CStream(); //デストラクタ(2) DECLARE_IUNKNOWN; //IUnknownに必要な関数を宣言するマクロ(3) STDMETHODIMP NonDelegatingQueryInterface(REFIID refiid,void **lplpInterface); //QueryInterfaceの実装(4) //実装するメソッドの宣言(5) STDMETHODIMP Seek(LARGE_INTEGER dlibMove,DWORD dwOrigin,ULARGE_INTEGER *plibNewPosition); STDMETHODIMP SetSize(ULARGE_INTEGER libNewSize); STDMETHODIMP CopyTo(IStream *lpStream,ULARGE_INTEGER cb,ULARGE_INTEGER *pcbRead,ULARGE_INTEGER *pcbWritten); STDMETHODIMP Commit(DWORD grfCommitFlags); STDMETHODIMP Revert(void); STDMETHODIMP LockRegion(ULARGE_INTEGER libOffset,ULARGE_INTEGER cb,DWORD dwLockType); STDMETHODIMP UnlockRegion(ULARGE_INTEGER libOffset,ULARGE_INTEGER cb,DWORD dwLockType); STDMETHODIMP Stat(STATSTG *lpStatStg,DWORD grfStatFlag); STDMETHODIMP Clone(IStream **lplpStream); STDMETHODIMP Read(void *pv,ULONG cb,ULONG *pcbRead); STDMETHODIMP Write(const void *pv,ULONG cb,ULONG *pcbWritten); protected: CStream(const std::string &strFileName,HANDLE hFile); //Cloneの実装用に用意されたコンストラクタ(6) private: std::string m_strFileName; HANDLE m_hFile; };
各部分を見ていきましょう。
クラス宣言
書き方はただのクラス宣言ですが、継承するものとしてこの場合は2つあります。それは
- IUnknownの機能を実装してあるDirectShowのヘルパクラスであるCUnknown
- 実装対象となるCOMオブジェクトのインターフェイスクラスであるIStream
です。前者はCOMオブジェクトを実装するときに残りの継承がすべてインターフェイスクラスであるときに使うと楽になるクラスです。
このクラスにはIUnknownの基本機能であるAddRef,Release,QueryInterfaceの基本動作を実装していますので楽をしたいときは継承してください。
継承しないときは自前でIUnknownの基本動作である三動作を実装する必要がありますので。
コンストラクタ(1)
コンストラクタです。基本的に引数はどういう形になってもいいのですが、はじめの二つ(lpOwnerとlphr)はCOMオブジェクトではほぼ必須となります。
lpOwnerはIUnknownの動作を制御するときに対象となるオブジェクトをあらかじめ指定する場合に使用します。この意味はあまりわからなくてもいいです。(=(詳しく言うなら、lpOwnerが指定されたとき、対象のクラスの参照カウント管理をlpOwnerに任せる、と言う動作になります。これはDirectShowのフィルタとピンのように参照が従属関係にある場合に参照カウント管理を親で受け持ちたい、と言うときに使用します。)=)
lphrにはコンストラクタが終了したときにそのコンストラクタが成功したのかどうかをHRESULTを使って返します。コンストラクタで失敗したときは即座にそのオブジェクトを解体しろ、という意味があります。
デストラクタ(2)
インターフェイスクラスを実装しているといっても実際はただの仮想継承を行っているクラスと変わらないのでデストラクタにはvirtualを記述しておく必要があります。
また、デストラクタはできるだけ宣言と実装を行うようにしてください。これの意味は後で説明します。
IUnknownに必要な関数を宣言するマクロ(3)
これはCUnknownを継承したときに必要となる記述です。自前で基本三動作を実装するなら必要がありませんがCUnknown(およびそれらを継承しているクラス)を実装するときには記述する必要があります。
QueryInterfaceの実装(4)
QueryInterfaceはCOMオブジェクトが持っている機能を公開するための関数です。この場合、このオブジェクトは2つの機能を持っていると思われます。
- COMのベースインターフェイスであるIUnknown
- 実装を行うインターフェイスであるIStream
IUnknownはすべてのCOMオブジェクトが持つべき機能と定められているのでこれは必須です。CUnknownではこの動作については実装済みとなっています。
後者は自前で実装したときもCUnknownを継承したときも公開対象となっていないので、それを公開するコードが必要となります。そのために実装が必要です。
自前で実装するときはQueryInterfaceという名前を使いますが、CUnknownを使ったときはNonDelegatingQueryInterfaceと「NonDelegating」がくっつきます。
あと、このメソッドは仮想関数にしてはいけません。これも理由がありますがこれは実装時に改めて。
実装するメソッドの宣言(5)
普通に宣言してください。これはクラスのメンバ関数の宣言と同じです。
ただし、戻り値を宣言するとき、「HRESULT」を返すメンバ関数を実装するときは「virtual __stdcall HRESULT」などと記述しないで「STDMETHODIMP」という宣言名を使用します。
そのまま「STanDard METHOD IMPlementation」(であっていたっけ?)という意味です。
また、メンバ関数の宣言は「元々のインターフェイスクラスで宣言しているものと全く同じ引数型で」宣言してください。ま、これはメンバ関数の継承でもいわれることですが。
Cloneの実装用に用意されたコンストラクタ(6)
Cloneメソッドを実装するときに必要となるコンストラクタです。この場合はコピーコンストラクタ的な役割を果たすものだと思っていいです。
コピーコンストラクタを実装することができないのでこんな形で実装しています。
CUnknownの継承による特性
CUnknownを継承することでインターフェイス系のクラスとして必要となる以下の処理が実装されます。
- コピーコンストラクタを実装することができなくなる
- コピーオペレータを実装することができなくなる
- デバッグ時に対象のオブジェクトに名前を割り振ることができ、それを確認することができる
前者二つはオブジェクトのコピー禁止を意味しています。インターフェイスがコピーできたらオブジェクトの状態が保てなくなるからですね。
同じインターフェイスを使うときはポインタを経由して使いましょう、という意味ですね。
まずは宣言だけです。次回は実装を簡単にやってみたいと思います。
そろそろ「難しくなってきた」頃だと思います。MSDNにも参考になる(ような)説明があると思いますので照らし合わせてみるといいかもしれません。