| Susie Plug-in Programming Guide |
このページでは、Susie Plug-in を利用するプログラムを作成するときに注意した方がいい点を説明しています。これらは、Susie Plug-in に対応したビューアである自作ツール KLARA を作成する上で得た経験によっています。 |
|
Network 上には非常に多くの susie plug-in があります。 これを利用しない手はありません。 Susie plug-in を自作のプログラムから利用するのは、 実際作ってみると判りますが、思ったよりも簡単です。
手順を簡単に説明すれば
え、手順の説明が簡単すぎるって? 確かにたったこれだけの説明で何をすればいいか判る人は、それなりの経験者だけだと思います。 しかし、このページではその具体的な対応の仕方を懇切丁寧に説明するのを目的にしているわけではありません。
では、一体ここでは何を説明しようとしているのでしょうか? それは、API 仕様だけに従っていても、上手く行かない場合がわりと沢山あると言うことです。 ここからは、Susie API 仕様だけからは判らない注意点を、具体的に説明していきます。 |
|---|
Susie Plug-in には、非常に多くの種類があります。
もちろんこれが大きな利点になっているのですが、同時に
非常に多くの人がプラグインを開発しているため、
微妙にその実装に違いが生じてきます。
そのため、以下に挙げるような点が問題になってきます。
また、実際にはそれがかなり有名な問題点である場合もあるのですが、 そんなことには最初は気付かないものです。 |
|---|
| 以下の項では、私が実際に遭遇した注意点を具体的に説明したいと思います。 ただし、これが問題点のすべてであると言っているわけではない点に注意して下さい。 |
|---|
|
非常に多くのAPIで、ファイル名などの文字列を引数として取りますが、
この文字列はすべて const char * ではなく char * であることに注意して下さい。 SUSIE APIに渡した文字列は、その API 内で変更されてしまう可能性があることを忘れてはいけません。 以下にこの例を示します。 |
|---|
int getPictureInfo( const char *name, DWORD flag, PictureInfo *lpInfo )
{
// name が変更されないことを保証します。
// tmp のサイズは十分である必要があります。
char tmp[512];
strcpy( tmp, name );
return( GetPictureInfo( tmp, flag, lpInfo ) );
}
|
| もくじに戻る |
|---|
|
いくつかの SUSIE API では、引数にファイル名を使用します。 例えば次のような場合です。 GetArchiveInfo( filename, 0, flag, &handle ) この時渡すファイル名が、既に別にオープンされている場合、 APIが失敗することが結構あります。 あなたがその対象ファイルを開くときに FILE_SHARE_READ をちゃんと設定していても、 プラグインは共有オープンを考慮しておらず、そのファイルの共有を認めてくれないかもしれません。 この様な場合、エラーコードが返されることが期待されますが、まれにプラグイン内部でのハングなどの、 より重篤な障害に発展することがあります。 よって、プラグインにファイル名を渡すときには、 オープン済みのファイル名は渡さない方が無難です。 以下に KLARA で使用している対策コードの例を示します。 |
|---|
class OnceCloseFile {
protected:
UINT mode;
CString path;
CFile &file;
public:
OnceCloseFile( CFile &f, UINT m )
: file(f), mode(m), path(f.GetFilePath())
{
file.Close();
}
~OnceCloseFile( void )
{
VERIFY( file.Open( path, mode ) );
}
};
int getArchiveInfo( CFile &file, HLOCAL *hInfo )
{
DWORD flag = 0; // read from DISK
int stat = 0; // no error
char tmp[MAX_PATH];
CString arcpath = file.GetFilePath();
OnceCloseFile ocf( file, CFile::modeRead|CFile::shareDenyWrite );
*hInfo = NULL;
strcpy( tmp, arcpath );
return( GetArchiveInfo( tmp, 0, flag, hInfo ) );
}
// MFC での file I/O は Serialize として呼び出されるため
// 対象(アーカイブ)ファイルは既にオープンされている
void CArcDoc::Serialize( CArchive &ar )
{
if ( ar.IsStoring() ){
ASSERT( FALSE );
Msg( "セーブはサポートしていない!" );
return;
}
HLOCAL hInfo = NULL;
if ( getArchiveInfo( *ar.GetFile(), &hInfo ) )
return; // some error
// * 後略 *
// アーカイブ内容一覧を表示するときのための処理が続く
}
|
| もくじに戻る |
|---|
|
これは、Susie 純正のプラグイン lhasad.spi に見られる、かなり有名な問題点です。 lhasad.spi を使うためには、この点を考慮に入れておく必要があります。 以下に KLARA で使用している対策コードの例を示します。 |
|---|
int getArchiveInfo( LPTSTR path, HLOCAL *hInfo )
{
DWORD flag = 0; // read from DISK
int stat = 0; // no error
*hInfo = NULL;
stat = GetArchiveInfo( path, 0, flag, hInfo );
if ( stat != 0 && stat != 2 )
return( stat );
if ( stat == 2 )
Msg( "GetArchiveInfo(): return value is 2!\n" );
if ( !*hInfo || ::LocalFlags( *hInfo ) == LMEM_INVALID_HANDLE )
return( 2 );
return( 0 );
}
|
| もくじに戻る |
|---|
|
これは、Susie 純正プラグイン lhasad.spi/axzip.spi/ifjpeg.spi 等で見られる有名なバグです。
この原因は、わたしの調査したところでは、LocalFree() に間違った HLOCAL を渡しているために発生しているようです。 WindowsNT の DEBUG 環境以外ではこのアサートは発生しませんが、 他の環境でもアサートが発生しないだけでこの問題は同様に発生しています。
通常は「デバッグがしにくい」くらいであまり実害はありません。 純正 axzip.spi では、このアサートが死ぬほど出るので、結構泣きそうになります。 考察はこちら。(上級者向き) |
|---|
|
もう一つ原因がハッキリしないんですが、IsSupported() や GetPicture() には成功するのに
GetPictureInfo() に必ず失敗するプラグインがあるような気がします。 ひょっとすると SUSIE は GetPictureInfo() をあまり重視しないような設計になっているのかも知れません。(これは憶測) GetPictureInfo() を先ず取得して次に GetPicture() を呼ぶようにするのは避けた方が無難かも知れません。 GetPictureInfo() に失敗しても1度は GetPicture() を呼ぶようにした方がよいのではないでしょうか。 もちろん IsSupported() がサポートしていると言っている場合に限りますが。 |
|---|
|
これは SUSIE純正 axzip.spi で、プラグイン非対応のZIPを開いたとき、GetFile()
を実行すると発生します。
具体的な症状としては、GetArchiveInfo() は正しい情報を返していますが、GetFile()
が HLOCAL を設定して正常終了コードを返すにもかかわらず、
HLOCAL が既に解放されているハンドルになっている、と言ったものになります。 よって、この問題を検出するため、GetFile() に次のような対策コードを加えておくと、 良いかも知れません。しかし、ディスクに展開した場合この問題をチェックできません。 |
|---|
int getFile( LPSTR name, const SpiFileInfo *lpInfo, HLOCAL *hImage )
{
int stat;
DWORD flag = 0x0100; // input = DISK, output = MEMORY
*hImage = NULL;
stat = GetFile( name, lpInfo->position, LPSTR( hImage ), flag, NULL, 0 );
if ( stat != 0 )
return( stat ); // error!
if ( ::LocalFlags( *hImage ) == LMEM_INVALID_HANDLE ){
Msg( "GetFile: BAD handle!" );
return( 5 );
}
return( 0 ); // success!
}
|
| もくじに戻る |
|---|
|
FLAG で出力を Disk / Memory のいずれかが選択できる API の場合、
どちらか片方しか正常に動作しないときがあります。そしてそのとき、
-1 ではない他のエラーコードが返ってきたりする事もしばしばあります。
これはおそらく、SUSIE が Disk / Memory どちらか片方しか使用していないため、
片方があまりデバッグされないままである場合が多いような気がします。 |
|---|
|
これは6や7の注意点にも関係しています。 |
|---|
|
いくつかの API では、処理の中断を可能にするため fnProgressCallback
を引数に取るものがあります。 このとき、fnProgressCallback の NULL 確認をせず、 有無を言わず呼び出してしまうプラグインがまれにあります。 このため、fnProgressCallback を NULL にするのではなくダミーの関数を渡すようにすると良いかも知れません。 例えば次のようにします。 |
|---|
int PASCAL __progressCallback( int nNum, int nDenom, long lData )
{
return( 0 ); // always continue
}
FARPROC getProgressCallback( FARPROC proc )
{
return( proc ? proc : FARPROC( __progressCallback ) );
}
int getPicture( LPSTR buf, long len, Flag flag,
HANDLE *pHBInfo, HANDLE *pHBm,
FARPROC lpProgress, long lData )
{
return( GetPicture( buf, len, flag,
pHBInfo, pHBm,
getProgressCallback( lpProgress ), lData ) );
}
|
| もくじに戻る |
|---|
GetPluginInfo() で plug-in API version を取得するとき、取得先バッファを
4バイトちょうどにするのは避けましょう。
char id[4]; GetPluginInfo( 0, id, sizeof( id ) )この様に記述した場合、4bytes ではなく 5bytes 書き込んでしまうプラグインがあります。 そのようなプラグインは、たとえば '00IN' ではなく "00IN" を常に返しているようです。 余分に書かれた 1byte の場所によっては、運が悪いとスタックの破壊などによりハングするかも知れません。 更に運が悪いと、非常に発見困難なバグを引き起こすかも知れません。 この様な問題を避けるため、GetPluginInfo() を呼ぶときは、 少し大きめにバッファを取っておいた方が安全でしょう。 例えば次のようにします。 char id[4+4]; GetPluginInfo( 0, id, 4 ) |
|---|
| もくじに戻る |
|---|
|
以上に挙げたような問題点を発見しやすくするためにも、
自作アプリがあっさりとハングするのは避けるようにした方が良いでしょう。 たとえば次のような感じにしておくと、少なくともプラグイン呼び出しでの即ハングは避けられます。 もちろん、プラグイン内での無限ループは回避不能ですが、 無限ループの場合はデバッガでとりあえずブレークできるので、 全くの原因不明にはなりづらいのではないでしょうか。 |
|---|
int getFile( LPSTR src,long len,LPSTR dest,unsigned int flag,
FARPROC prgressCallback,long lData )
{
try {
return( GetFile( src, len, dest, flag, progressCallback, lData ) );
}
catch( CException *e ){
#ifdef _DEBUG
e->ReportError();
#endif
e->Delete();
Msg( "GetFile: some exception!" );
}
catch( ... ){
// あらゆる例外を処理します。
// たとえ "ページ例外(int 0Ch)" や "一般保護例外(int 0Dh)" が発生した
// としても、ハングは避けることが出来ます。
Msg( "GetFile: unexpected exception!" );
}
return( 8 ); // internal error!
}
|
| もくじに戻る |
|---|
|
さいごに、プラグインを利用するプログラムを作成するときの、
全般的な注意点を少し挙げておきたいと思います。
初級編
中級編
上級編
|
|---|
|
以上が、私の体験した罠の数々です。 これらが、なにかの参考になれば幸いです。 もし、内容に間違いがあると気づいた場合には、お知らせください。 検討の後に修正します。 なお、参考としてあげたコードは、わかりやすく模式的に変更されたもので、 KLARA で実際に使用されているコードそのままではありません。 これは KLARA はほぼ完全に C++ で記述されているため、 そのままのコードでは非常に説明がしにくいためです。
|
|---|