Susie Plug-in Programming Guide

このページでは、Susie Plug-in を利用するプログラムを作成するときに注意した方がいい点を説明しています。
これらは、Susie Plug-in に対応したビューアである自作ツール KLARA を作成する上で得た経験によっています。
また、Susie Plug-in を利用するための基本事項を説明することを目的とはしていません。

もくじ




Step1: Susie Plug-in に対応してみよう!

Network 上には非常に多くの susie plug-in があります。 これを利用しない手はありません。

Susie plug-in を自作のプログラムから利用するのは、 実際作ってみると判りますが、思ったよりも簡単です。

手順を簡単に説明すれば

  1. 目的の Plug-in を LoadLibrary() を使ってロードする。
  2. 使用したい Plug-in API のアドレスを GetProcAddress() を使って取得する。
  3. 必要なときにそれら API を適切な引数を与えて呼び出す。
たったこれだけで、Susie Plug-in を自由に使用することが出来ます。

え、手順の説明が簡単すぎるって?

確かにたったこれだけの説明で何をすればいいか判る人は、それなりの経験者だけだと思います。 しかし、このページではその具体的な対応の仕方を懇切丁寧に説明するのを目的にしているわけではありません。

Susie Plug-in API に関する詳しい説明に関しては、 既にネット上に良いページがありますので、そちらを参照して下さい。

おすすめは、kana's Home Page の「plug-in関連工作室」です。 Susie Plug-in API のリファレンスが解説されています。
もちろん、純正 SPI_API.TXT もちゃんと入手して一読しましょう。


では、一体ここでは何を説明しようとしているのでしょうか?

それは、API 仕様だけに従っていても、上手く行かない場合がわりと沢山あると言うことです。

ここからは、Susie API 仕様だけからは判らない注意点を、具体的に説明していきます。




Step2: なぜ仕様通りに行かない場合があるのか?

Susie Plug-in には、非常に多くの種類があります。 もちろんこれが大きな利点になっているのですが、同時に 非常に多くの人がプラグインを開発しているため、 微妙にその実装に違いが生じてきます。 そのため、以下に挙げるような点が問題になってきます。
  1. Susie が利用しているAPIしか実装されていない。
  2. Susie が利用しているAPIしかデバッグされていない。
  3. Susie が呼び出さないAPIの仕様を、誤解して実装している。
  4. Susie では顕在化しないバグが含まれている。
  5. 正常終了しているにもかかわらずエラーコードを常に返す。
  6. エラーが発生しているにもかかわらずエラーコードを正しく返さない。
Susie Plug-in に対応したプログラムを初めて作成しているときに、 運悪くAPI仕様からはずれた挙動をするプラグインに遭遇すると、 まさかプラグインの方に問題があるとはなかなか気づかず、 延々と悩み続ける羽目になります。

また、実際にはそれがかなり有名な問題点である場合もあるのですが、 そんなことには最初は気付かないものです。




Step3: 具体的な問題点

以下の項では、私が実際に遭遇した注意点を具体的に説明したいと思います。 ただし、これが問題点のすべてであると言っているわけではない点に注意して下さい。



Case1: 引数に渡す文字列は LPCSTR ではなく LPSTR である

非常に多くの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 ) );
}
もくじに戻る


Case2: 既に OPEN しているファイル名を渡すのは避ける

いくつかの 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

	// * 後略 *
	// アーカイブ内容一覧を表示するときのための処理が続く
}
もくじに戻る


Case3: GetArchiveInfo() が常にエラーコード2を返すにもかかわらず、
実際にはそのAPIは正常終了している

これは、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 );
}
もくじに戻る


Case4: WindowsNT 上でデバッグをしていると、int 3 で停止する

これは、Susie 純正プラグイン lhasad.spi/axzip.spi/ifjpeg.spi 等で見られる有名なバグです。

この原因は、わたしの調査したところでは、LocalFree() に間違った HLOCAL を渡しているために発生しているようです。

WindowsNT の DEBUG 環境以外ではこのアサートは発生しませんが、 他の環境でもアサートが発生しないだけでこの問題は同様に発生しています。

通常は「デバッグがしにくい」くらいであまり実害はありません。
が、ごくまれに LocalFree() に HLOCAL として認識されてしまうような誤ったハンドルが渡ってしまう可能性があるかもしれません。
このような場合には予期しない結果を招くのかも知れません。

純正 axzip.spi では、このアサートが死ぬほど出るので、結構泣きそうになります。

考察はこちら。(上級者向き)



Case5: GetPictureInfo() でいつもエラーを返すプラグインがある
(ような気がする)

もう一つ原因がハッキリしないんですが、IsSupported() や GetPicture() には成功するのに GetPictureInfo() に必ず失敗するプラグインがあるような気がします。
ひょっとすると SUSIE は GetPictureInfo() をあまり重視しないような設計になっているのかも知れません。(これは憶測)

GetPictureInfo() を先ず取得して次に GetPicture() を呼ぶようにするのは避けた方が無難かも知れません。 GetPictureInfo() に失敗しても1度は GetPicture() を呼ぶようにした方がよいのではないでしょうか。 もちろん IsSupported() がサポートしていると言っている場合に限りますが。



Case6: エラーコードを返さなくてもエラーが発生しているときがある

これは SUSIE純正 axzip.spi で、プラグイン非対応のZIPを開いたとき、GetFile() を実行すると発生します。

具体的な症状としては、GetArchiveInfo() は正しい情報を返していますが、GetFile() が HLOCAL を設定して正常終了コードを返すにもかかわらず、 HLOCAL が既に解放されているハンドルになっている、と言ったものになります。
GetFile() でディスクに展開した場合にも正常終了しそのファイルは作成されますが、 ファイルの内容は正常に生成されていません。

よって、この問題を検出するため、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!
}
もくじに戻る


Case7: 出力先を Disk / Memory が選択できるAPIで、
どちらか片方への出力しか正常に動作しない場合がある

FLAG で出力を Disk / Memory のいずれかが選択できる API の場合、 どちらか片方しか正常に動作しないときがあります。そしてそのとき、 -1 ではない他のエラーコードが返ってきたりする事もしばしばあります。

これはおそらく、SUSIE が Disk / Memory どちらか片方しか使用していないため、 片方があまりデバッグされないままである場合が多いような気がします。
この様な場合、正常に動作する方のみを使用するようにする必要があります。



Case8: GetFile() はメモリーに展開するようにする

これは6や7の注意点にも関係しています。
6や7で説明したような理由により、Disk に展開するのに失敗するプラグインをいくつか確認しています。
Disk に展開したい場合には、メモリに展開した後、自力でファイルを作成するようにした方が、より安全です。



Case9: fnProgressCallback は NULL ではなく、
ダミー関数を渡した方がより安全

いくつかの 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 ) );
}
もくじに戻る


Case10: GetPluginInfo() では余裕を持ってバッファを取る

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 )
もくじに戻る


Case11: API の呼び出し中は例外処理をしておくと、
プラグイン呼び出しであっさりハングするのを
避けることが出来る(C++の場合に限る)

以上に挙げたような問題点を発見しやすくするためにも、 自作アプリがあっさりとハングするのは避けるようにした方が良いでしょう。

たとえば次のような感じにしておくと、少なくともプラグイン呼び出しでの即ハングは避けられます。

もちろん、プラグイン内での無限ループは回避不能ですが、 無限ループの場合はデバッガでとりあえずブレークできるので、 全くの原因不明にはなりづらいのではないでしょうか。


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!
}
もくじに戻る


Step4: うまく動かないときには

さいごに、プラグインを利用するプログラムを作成するときの、 全般的な注意点を少し挙げておきたいと思います。

初級編

  1. 開発言語(例えばC言語)を正しく理解していますか?
    言語を正しく理解していないと、思わぬバグに悩まされることになるでしょう。
  2. SPI_API.TXT を良く読みましょう。 最初は勘違いなどをしがちです。
  3. コードを良く見直してみましょう。 最初のうちはプラグインよりも、 自作のプログラムの方がはるかに信用できません。

中級編

  1. SUSIEでそのプラグインが正常動作しているか確認しましょう。
    SUSIEで動作している場合には、何とかして動作させる方法があるはずです。
  2. あなたが試しているそのファイルは、そのプラグインで本当に開けるファイルですか?
    SUSIEでそのファイルが開けるかどうか確認してみましょう。 そのファイルが壊れている場合や、非対応である場合もあります。
  3. あるプラグインで上手く動作しない場合、他のプラグインでも試してみましょう。
    そのプラグインがたまたま変なのかも知れません。

上級編

  1. 上手く行かないときには、引数を少し変えてみましょう。今度は上手く行くかもしれません。
  2. APIの返すエラーコードを信用しすぎてはいけません。 参考程度にとどめておいた方が良いかも知れません。
  3. プラグインにもバグはあります。バグを回避する方法を考えましょう。
  4. プラグインの挙動が不審だと疑った場合、デバッガでプラグイン内までトレースすると原因が分かることがあります。 (但しプラグインやAPIにもよります)
  5. プラグインを逆アセンブルすると、比較的簡単に原因を特定できる場合があります。 (但し開発言語によります)



さいごに

以上が、私の体験した罠の数々です。
これらが、なにかの参考になれば幸いです。

もし、内容に間違いがあると気づいた場合には、お知らせください。 検討の後に修正します。

なお、参考としてあげたコードは、わかりやすく模式的に変更されたもので、 KLARA で実際に使用されているコードそのままではありません。 これは KLARA はほぼ完全に C++ で記述されているため、 そのままのコードでは非常に説明がしにくいためです。



戻る


[TopPage]  [CG.Works]  [KLARA]  [Program]  [BBS]  [Links]