残りは実装だけです。さくっとやってみましょう。
実装しないものはE_NOTIMPLを返して終わらせちゃおう
COMの関数の中には「関数は用意されているけど機能がないんだよ~」的なものが意外とたくさんあります。いわゆる、拡張用ですね。
また、今回のように「読み込み専用なので書き込みの機能は実装しないよ」という意味で実装しないものもあります。
それらはあっさりと実装することができます。
今回は読み込み専用なので書き込みに関する機能を全部無視します。IStreamで読み込みに関する機能を持つものは
Seek | 読み取り位置を移動する |
Stat | IStreamの状態を取得する(実装側から見ると返す) |
Clone | 同じ状態のIStreamオブジェクトを複製する |
Read | 現在のカーソル位置からデータを読み出す |
の4つだけなのでほかはさくっと未実装ということで終わらせます。
本来ならもう一つCopyTo(対象のIStreamにデータをコピーする)も実装対象とすべきですが、面倒なので実装しません。
STDMETHODIMP CStream::SetSize(ULARGE_INTEGER libNewSize) { return E_NOTIMPL; } STDMETHODIMP CStream::CopyTo(IStream *lpStream,ULARGE_INTEGER cb,ULARGE_INTEGER *pcbRead,ULARGE_INTEGER *pcbWritten) { return E_NOTIMPL; } STDMETHODIMP CStream::Commit(DWORD grfCommitFlags) { return E_NOTIMPL; } STDMETHODIMP CStream::Revert(void) { return E_NOTIMPL; } STDMETHODIMP CStream::LockRegion(ULARGE_INTEGER libOffset,ULARGE_INTEGER cb,DWORD dwLockType) { return E_NOTIMPL; } STDMETHODIMP CStream::UnlockRegion(ULARGE_INTEGER libOffset,ULARGE_INTEGER cb,DWORD dwLockType) { return E_NOTIMPL; } STDMETHODIMP CStream::Write(const void *pv,ULONG cb,ULONG *pcbWritten) { return E_NOTIMPL; }
簡単ですね~。
実装するときの注意点
実装するときには以下の点に注意して組むようにしてください。
引数チェックを厳しく | 特にポインタがNULLを許可しないときにはE_POINTERを返してポインタエラーであることを必ず通告すること。 |
内部状態の監視も厳しく | 内部データが有効でないのに処理を行おうとしないこと(確実に大丈夫という状態ならいいが) |
エラーコードを正しく返す | エラーコードは汎用のもの以外で指定があるときはそちらを使うようにする。 |
Readの実装
そのまんまです。ま、気をつけるべき点に気をつけていきます。
STDMETHODIMP CStream::Read(void *pv,ULONG cb,ULONG *pcbRead) { BOOL bRet; DWORD dwReadSize; CheckPointer(pv,STG_E_INVALIDPOINTER); //CheckPointer(pcbRead,STG_E_INVALIDPOINTER); if(m_hFile == INVALID_HANDLE_VALUE) return E_HANDLE; bRet = ::ReadFile(m_hFile,pv,(DWORD)cb,&dwReadSize,NULL); if(pcbRead != NULL){ *pcbRead = (ULONG)dwReadSize; } if(!bRet) return E_FAIL; return S_OK; }
そのまんまといっているくせにいろいろとやかましいコードがくっついていますね。
まずは引数チェックです。IStreamのReadのヘルプには「pvはエラーでない限りNULLにはならない」とあるので、ポインタチェックが必要です。
CheckPointerはポインタがNULLの時は第二引数を返すマクロで、NULLチェックにはぴったりです。DirectShowのマクロですが・・・。
DirectShowなどでは「ポインタエラーはE_POINTERを返すように」となっていますが、IStreamではそれがSTG_E_INVALIDPOINTERになっていますのでそちらを使っています。
どちらにしてもエラーなのは変わりないわけですが・・・。
で、もう一つのポインタは「pcbReadはNULLに設定できる」とあるのでこれはNULLチェック対象ではないですね。
そして内部状態の監視です。これは念のために入れてありますが、ハンドルが無効の時はE_HANDLEを返してハンドルエラーであることを通知しています。
で、やっとReadFileです。さっきの「pcbReadはNULLに設定できる」との記述があるのでそのままReadFileには渡せなくなるので中間変数を挟んでいます。
後は終了処理なのでわかりやすいと思います。最後に成功したときはS_OKで読み込み成功を返して完了です。
Seekの実装
Seekもそのまんま実装します。
STDMETHODIMP CStream::Seek(LARGE_INTEGER dlibMove,DWORD dwOrigin,ULARGE_INTEGER *plibNewPosition) { BOOL bRet; DWORD dwMoveMethod; DWORD dwSeekPos; if(m_hFile == INVALID_HANDLE_VALUE) return E_HANDLE; switch(dwOrigin){ case STREAM_SEEK_SET: dwMoveMethod = FILE_BEGIN; break; case STREAM_SEEK_CUR: dwMoveMethod = FILE_CURRENT; break; case STREAM_SEEK_END: dwMoveMethod = FILE_END; break; default: return STG_E_INVALIDFUNCTION; } dwSeekPos = ::SetFilePointer(m_hFile,dlibMove.LowPart,&dlibMove.HighPart,dwMoveMethod); if(dwSeekPos == INVALID_SET_FILE_POINTER && GetLastError() != NO_ERROR) return E_FAIL; if(plibNewPosition != NULL){ plibNewPosition->LowPart = dwSeekPos; plibNewPosition->HighPart = dlibMove.HighPart; } return S_OK; }
ポインタチェックは「plibNewPositionはNULLに設定できる」とのことなのでしなくてもいいですね。
問題はdwOriginで、これは説明では三つの値をとる、とありますが、これはSetFilePointerの定数と互換があるかわからないので変換します。
このときに三つの値のいずれでもないときはヘルプのreturn値に従ってSTG_E_INVALIDFUNCTIONを返します。
あとはSetFilePointerを呼び出してSeekさせます。失敗したときはE_FAILで失敗したことにします。
Cloneの実装
そのまんまです。
STDMETHODIMP CStream::Clone(IStream **lplpStream) { CStream *lpStream; LARGE_INTEGER li; HRESULT hr; CheckPointer(lplpStream,STG_E_INVALIDPOINTER); lpStream = new CStream(GetOwner(),&hr,m_strFileName.c_str()); if(hr != S_OK){ delete lpStream; return hr; } li.QuadPart = 0; li.LowPart = ::SetFilePointer(m_hFile,0,&li.HighPart,FILE_CURRENT); hr = lpStream->Seek(li,STREAM_SEEK_SET,NULL); if(hr != S_OK){ delete lpStream; return hr; } hr = lpStream->QueryInterface(IID_IStream,(void **)lplpStream); if(hr != S_OK){ delete lpStream; return hr; } return hr; }
・・・そのまんまでもないか。コピーコンストラクタに近いコンストラクタを使えればいいんですが、DuplicateHandleだとファイルカーソルが共有してしまうでファイルを再度開かせる方法でコピーしています。
再度正しく開ければファイルカーソルをコピーしてオブジェクトをアップキャスト(QueryInterface)して完了です。
エラー処理がちょっと微妙ですが・・・。
Statの実装
STDMETHODIMP CStream::Stat(STATSTG *lpStatStg,DWORD grfStatFlag) { CheckPointer(lpStatStg,STG_E_INVALIDPOINTER); if(m_hFile == INVALID_HANDLE_VALUE) return E_HANDLE; memset(lpStatStg,0x00,sizeof(STATSTG)); if(!(grfStatFlag & STATFLAG_NONAME)){ #ifdef _UNICODE lpStatStg->pwcsName = (LPOLESTR)CoTaskMemAlloc((ULONG)((m_strFileName.size() + 1) * sizeof(WCHAR))); lstrcpyW(lpStatStg->pwcsName,m_strFileName.c_str()); #else int len = MultiByteToWideChar(CP_ACP,0,m_strFileName.c_str(),-1,NULL,0); lpStatStg->pwcsName = (LPOLESTR)CoTaskMemAlloc((ULONG)(len * sizeof(WCHAR)); MultiByteToWideChar(CP_ACP,0,m_strFileName.c_str(),-1,lpStatStg->pwcsName,len); #endif } lpStatStg->type = STGTY_STREAM; lpStatStg->grfMode = STGM_READ; lpStatStg->cbSize.LowPart = ::GetFileSize(m_hFile,&(lpStatStg->cbSize.HighPart)); ::GetFileTime(m_hFile,&(lpStatStg->ctime),&(lpStatStg->atime),&(lpStatStg->mtime)); return S_OK; }
ファイル名のコピーがとても大変ですがこんな感じですね。
ファイル名のバッファはCoTaskMemAllocで確保する、UNICODEである、という定義があるのでその定義に従っています。
UNICODEかどうかでstringを分けてどうにかしています。かなり面倒ですね・・・。
なお、VisualStudioでstd::stringというと勝手にcharの文字列になりますので注意してください。std::stringと宣言していますが、
UNICODEの状態によって定義が変わるようになっていると仮定しています。(つまり、stringのメンバ関数c_strの戻り値をconst TCHAR *と仮定しています)
それ以降のデータは構造体中の必要な部分にデータを書き込んでいるだけです。
というわけで実装してみました。試しに使ってみるといいかもしれません。
IStreamの定義はいろんなもの(自作ライブラリのファイル処理にも)に転用できるのでこういうのを考えることはおもしろいと思います。
次回からはしばらくこの系の記事はお休みして別の話をしてみたいと思います。何となくです・・・。