一度できたけど、もう一回やろうとすると手順を忘れてしまいそうな作業内容の記録です。基本的に貧弱な開発環境、数回しか行わない操作が対象です。あとは感想とか。
Visual Studio 2008 ExpressでJust-In-Timeデバッグに対応する
Standardエディション以上ならJust-In-Timeデバッグのオプションがあるらしいけど、Expressでは使えないらしい(根本的な解決策は、Standard以上を購入することだけど、そんなに使わないし)。レジストリでなんとかならないかと思ったけど、Just-In-Time用のバイナリが指定されているので簡単ではなさそう。頻繁にデバッグを行うのではなく、例外発生時のデバッガ指定でVS2003が表示される環境なら、以下の順でデバッガを切り替えればOK。Windows
XP(32ビット)で確認済。他の環境では一部変更が必要だと思う。
1) 例外発生時のダイアログが表示された時点でVS2008を起動し、例外が発生したプロセスにアタッチする。
2) ”全て中断”で全てのスレッドを停止しておく(「プロセスがデッドロック…」いうメッセージが表示されるのでOKを押す)。
3) ダイアログの「デバッグ」ボタンを押す。
4) VS2008の「続行」を選び、実行を再開する。
5) OSがJust-In-Timeデバッガ(VS2003)を起動しようとするが、VS2008が既にアタッチしているのでエラーになり、関連するメッセージが表示されるのでOKを押す。
6) VS2008に制御が移り、例外が発生した地点でブレイクが掛かる。
VS2003がインストールされている場合はこれでOKだけど、Just-In-Timeデバッガが存在しない場合は、+レジストリ情報書き換えでなんとかなるかも。その時が来たら挑戦してみるけど。
とか書いてる間にできてしまったので、続きを書きます。Just-In-Timeデバッガが存在しない場合は、レジストリ書き換えで対応する(.NET環境も含めた詳細は、MSDNの「Just-In-Time
デバッグ」などを参考にして下さい。個人的にネイティブ環境でさえ使えればいいので)。
*1) レジストリに”HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows NT\CurrentVersion\AeDebug\Debugger”キーを作成する。
*2) 以下の値を作成する。
Auto="0"(文字列値)
Debugger="drwtsn32 -p %ld -e %ld -g"(文字列値)
UserDebuggerHotKey=0(DWORD値)
*3) 後は、前述の1)からの操作を行うだけ。レジストリを作成した場合は、4)でエラーメッセージが基本的に表示されなくなる(良くなっているのだが、表示される場合もあった)。
追加された機能らしい。知らなかった。Dependency Walkerで遅延読み込みがなんとかとか表示されるから存在は知っていたけど。リンカオプションの/DELAYLOADで遅延読み込みをするDLLを指定、/DELAYで遅延読み込みを制御する(明示的なアンロードを有効にしたり、バインド可能なインポートアドレステーブルを作成するか(しない場合は、GetProcAddress()が毎回呼ばれる感じ)など)。遅延読み込みを利用すれば、OSによっては存在しないDLL(テーマ系のDLLとか)の関数を呼び出すコードを含む実行ファイルの起動時のエラーはなくなりそう。ただ、関数呼び出しの部分に制御が達した時にDLLが読み込めないとエラー(例外)になる。結局、どうするかというとLoadLibrary(
)やGetProcAddress( )でDLLのロードを確認することになるみたい。あんま、意味ないような。もうちょっと読んでみたけど、フックとか余計に複雑な気もする。
結局、呼び出す関数が少なければ、LoadLibrary( )を直に呼び出して制御した方が楽そう。呼び出す関数が多ければ、このオプションを利用した方が楽そうって感じ。テーマ系のAPIをバリバリに使う場合はいいかも。他は第3者によるDLLを利用する場合とか、エラーメッセージの表示を自分でコントロールできるから便利かな。でも、私には関係ないか。
仕事でやっている人なら色々あるでしょうけど。美しいコーディングとか、結局、時代や言語やマシンのスペック、要は環境に依存するものだし。一昔前なら、メモリが高価で搭載量も少ないから、メモリ使用量とかコード量が少なく処理が早いのが理想だろうけど、今はメモリは余るほどあって、処理時間よりも待機時間の方が長いくらいだから、高度な科学計算を必要とするのでなければ理想でもない。むしろ、メモリの使用量を増大させて、コードも複雑で実行時間が掛かるプログラムにして、ユーザーにマシンの方を買い換えさせようとする方が消費社会のビジネスとして良いみたいな風潮すらあるし。人間が読むコードが美しかろうと、汚かろうと、マシン語になった時点で同じなら処理結果は同じだし。どこに基準を置くかで変わってくる。前は、
FILE* fp = NULL;
if( !( fp = fopen( "hoge.txt", "r" ) ) ) return -1;
みたいなコードを書いてたけど、Visual Studioを使い出してからは、デバッガと最適化が強力だから、Visual Studioが理解しやすいようなコードを書くようになった。エレガントではないのですけど。
FILE* fp = NULL;
fp = fopen( "hoge.txt", "r" );
if( fp == NULL )
return -1;
アセンブラのように1行1コマンド。1行をなるべく短くして、インテリセンスが解析しやすいように、デバッグブレイクで変数の変化を追いやすいように書くようになった。Linuxハッカーとかが好んで書くようなスタイルは避けるようになった。デバッグビルドならともかく、コンパイラに最適化させたら上のコードはどっちのコードが簡潔になるかわからないし。以前、ポインタと添え字配列を使った同じ文字列操作を行うコードで検証したことがあるけど、ポインタを使った方が速いかというとそうでもなかった(最適化した場合は逆でした)。推奨されているコーディングスタイルがいつまでも正しいかって点には疑問を持つべきだと思う。個人的には、バイナリになった時点で問題なければOK。エラーや例外発生箇所が特定しやすいコードを書くようには心掛けてるけど。
C言語に拘る理由も特にないけど。C言語の利点は、明らかに遅いだろうなって書き方をしても、JavaとかVBAとか.NETに比べれば速いことが多いから手抜きができることか。かなり遅く書いても速くなってしまう。N225Traderとか、データを文字列で識別してるけど(エラーが発生した時にデバッグが楽なので)、もしインデックスアクセスなら、まだ少なくとも2倍以上は高速化できる余裕があるし。便利だけど、プロパティとかユーザーインターフェイス周辺の生産性が低いので嫌になるけど。
最後に、ほとんど同じ内容のAとB.、何故かよく見かける自家製strcpy( )ですが、どちらで記述しますか。私はBです(何でかというと、Bの方がエラーが発生した場合にデバッグが楽だと思うので。ポインタの開始アドレスにに関係なくインデックス値でエラー位置を特定できるから。あと、BならJava使いでもVB使いでも理解できるコードだし。でも普通はAか。勿論、正解はなくて好みの問題です。他の書き方をされる方もいらっしゃるでしょうね)。
/* A */
void funcA( char *d, const char *s )
{
while( *s || ( *d = '\0' ) )
*d++ = *s++;
}
/* B */
void funcB( char *d, const char *s )
{
int i = 0;
for( ; s[ i ] != '\0' ; i++ )
d[ i ] = s[ i ];
d[ i ] = '\0';
}
因みに、Visual Studio 2008のリリースビルドの既定の最適化(実行速度:/O2)コードは以下になりました。パッと見でBの方が長いですけど、ループ部分(赤)を比べても、明らかにAの方が勝っています。Bの方は、ちょっと酷いですね。もう少し、まともなコードを吐くかと思いましたけど。結論としては、やはり、ポインタは速かったということになりました。個人的には、ちょっと考え方を改めなくてはならない結果になりました。
__declspec( dllexport ) void funcA( char *d, const char *s )
{
mov edx,dword ptr [esp+8]
mov al,byte ptr [edx]
test al,al
je funcA+1Dh
mov ecx,dword ptr [esp+4]
mov edi,edi
inc edx
mov byte ptr [ecx],al
mov al,byte ptr [edx]
inc ecx
test al,al
jne funcA+10h
mov eax,dword ptr [esp+4]
mov byte ptr [eax],0
ret
}
__declspec( dllexport ) void funcB( char *d, const char *s )
{
mov eax,dword ptr [esp+8]
mov cl,byte ptr [eax]
push esi
xor esi,esi
test cl,cl
je funcB+28h
push edi
mov edi,dword ptr [esp+0Ch]
mov edx,edi
sub edx,eax
mov byte ptr [edx+eax],cl
mov cl,byte ptr [eax+1]
inc eax
inc esi
test cl,cl
jne funcB+16h
mov byte ptr [esi+edi],cl
pop edi
pop esi
ret
mov eax,dword ptr [esp+8]
mov byte ptr [esi+eax],0
pop esi
ret
}
nvplayer.exe(Nave the BK Player)でFLVファイルを再生しようとして、コーデックまでインストールしたついでに、有名なK-Liteコーデックパックで一通りのコーデックもインストールしたけど、2ヶ国語音声のMKVファイルで日本語出力が選択できなくなってしまった。以前はffdshowで日本語をいう選択肢があったのだが。コーデックの仕組みもわからないのでどうすることもできない。とりあえず、K-LiteコーデックパックのHelpページに「走査が遅い場合は、GabestではなくHaaliを使え」、「2ヶ国語で音声を選択する場合は、Haaliの白いトレイアイコンをクリックしろ」という記述があった。白いアイコンなんて表示されないので、一旦アンインストールし、再インストール時にAVI SplitterでHaaliを選択(デフォールトはGabest)したら日本語音声が選択できるようになった(白いアイコンが表示されるようになった)。レジストリなんか多分グチャグチャだろうけど、それでも動くのがWindows。素晴らしい。
ちなみに最近メインマシンがウィルスに感染していることがわかった。正確にはウィルスではなく、ワームとマルウェアの中間辺りだと思うけど。初めて検出した時は、どこから進入したのか不思議だったけど、どうもFlashGet経由で取り込まれたらしい(NATでポートもほとんど塞がっているいる所に侵入したのだからたいしたものだと思ったけど、そうではなかった)。とりあえず手動で削除して、動作が不審じゃなきゃいいやくらいでいるけど。少なくともウィルスじゃなくて良かった。ウィルスだったらOSをクリーンインストールせざるを得なかったかも。最近はコンピュータも複雑になって、人間と同じくウィルス(寄生体)も普通に潜んでいるから、悪性じゃなければいいかくらいに考えが緩くなってきた。
浮動小数点の文字列表現("0.2345E+50"とか)やC言語のシンボルを認識するのにis系の関数とループを使って、その都度実装するのが面倒なので簡単な正規表現を扱えるオブジェクトを作ろうと思った。如何せん、情報工学の基礎知識が欠如しているのでアルゴリズムをどうすればいいのか、全くわからない。最低限度の知識はWikiで、その他は有用な情報をウェブ上で提供してくださっているプログラマの方々の知恵を拝借してなんとかしようと思った。とりあえず、キーワードは、正規表現(当たり前か)、再帰降下法、非決定性有限オートマトン(NFA)、決定性有限オートマトン(DFA)。
### 正規表現 ###
とは、一般的には”[a-zA-Z_][a-zA-Z_0-9]*”みたいな表現のことだけど、純粋には、連結(Concatenation)、選択(Union)、閉包(Closure)によって再帰的に定義されるものらしい。要は、この3つ基本操作以外は糖衣構文みたいなものになる(のかな)。”[0-9]”は”0|1|2|3|4|5|6|7|8|9”のことだし。逆に言えば、この3つを実装すれば、正規表現の実装も終わり。この3つが核で他はおまけ又はオプション。
### 非決定性有限オートマトンと決定性有限オートマトン ###
突然、変な言葉が出てきた。何故、正規表現を実装するのにこんな道具が必要なのかと普通は考えると思うのだが(私だけでしょうか)。言葉だけ並べると、非決定性オートマトンとは、ある入力を受け取った時に次の遷移先が一意的に決まらないようなオートマトンのこと、決定性オートマトンは次の遷移先が一意的に決まるオートマトンのこと。有限、無限は入力の種類が有限か無限かということ。で、ある証明された事実があって、任意の非決定性有限オートマトン(NFA)には等価な決定性有限オートマトン(DFA)が存在する。この事実を何に利用したいかというと、バックトラックを排除して、検索、又は実行の高速化を図るため。高速化の必要がなければ、DFAを利用する必要はない(実際にDFAに変換しない正規表現ライブラリもありますし、否定(Not)表現(純粋な正規表現で言うところのある要素以外の全ての要素の選択(Union))しかない場合は、変換に時間が掛かる上に変換しても遅い(はずですけど))。正規表現がたまたま要件を満たしているから学者のおもちゃになっているだけなのかも。
具体例がないとピンとこないので、書き出してみる。例えば、”0|[0-9]*0”という正規表現を忠実に関数化すると、
--------------------------------------------------
int Match0(const char* psz )
{
int i = 0;
if( psz[ i ] == '\0' || psz[ i++ ] != '0' )
return 0;
if( psz[ i ] != '\0' )
return 0;
return 1; /* Matched */
}
int MatchLast(const char* psz )
{
int i = 0;
char c = '\0';
while( isdigit( c = psz[ i++ ] ) )
continue;
if( psz[ i ] == '\0' && c == '0' )
return 1; /* Matched */
return 0;
}
int Match( const char* psz )
{
return ( Match0( psz ) || MatchLast( psz ) );
}
--------------------------------------------------
となる(これは人によって違うかも)。とりあえず、前の表現に一致するか試してみて、駄目なら後ろの表現をチェックするような実装になっている。同じく、”0|01|[0-9]*0”という正規表現を忠実に関数化すると、
--------------------------------------------------
int Match1(const char* psz )
{
int i = 0;
if( psz[ i ] == '\0' || psz[ i++ ] != '0' )
return 0;
if( psz[ i ] == '\0' || psz[ i++ ] != '1' )
return 0;
if( psz[ i ] != '\0' )
return 0;
return 1; /* Matched */
}
int Match( const char* psz )
{
return ( Match0( psz ) || Match1( psz ) || MatchLast( psz ) );
}
--------------------------------------------------
となる。同じく、”0|01|012|[0-9]*0”という正規表現を忠実に関数化すると、
--------------------------------------------------
int Match2(const char* psz )
{
int i = 0;
if( psz[ i ] == '\0' || psz[ i++ ] != '0' )
return 0;
if( psz[ i ] == '\0' || psz[ i++ ] != '1' )
return 0;
if( psz[ i ] == '\0' || psz[ i++ ] != '2' )
return 0;
if( psz[ i ] != '\0' )
return 0;
return 1; /* Matched */
}
int Match( const char* psz )
{
return ( Match0( psz ) || Match1( psz ) || Match2( psz ) || MatchLast(
psz ) );
}
--------------------------------------------------
となる。同様の手順で、”0|01|012|0123|01234|012345|0123456|01234567|012345678|0123456789|[0-9]*0”という正規表現を忠実に関数化すると、
--------------------------------------------------
int Match( const char* psz )
{
return ( Match0( psz ) || Match1( psz ) || Match2( psz ) || Match3( psz
) || Match4( psz ) || Match5( psz ) || Match6( psz ) || Match7( psz ) ||
Match8( psz ) || Match9( psz ) || MatchLast( psz ) );
}
--------------------------------------------------
各MatchXXX関数が真の値を返さないと最初に戻って次の関数を試す(バックトラック)というあまりにも馬鹿らしいコードなので、ちょっとだけ最適化してみる。
--------------------------------------------------
int Match( const char* psz )
{
int i = 0, partial_match = !0;
char c = '\0';
if( !isdigit( c = psz[ i++ ] ) )
return 0;
partial_match &= ( c == '0' );
if( psz[ i ] == '\0' && ( c == '0' || partial_match ) )
return 1; /* Matched */
if( !isdigit( c = psz[ i++ ] ) )
return 0;
partial_match &= ( c == '1' );
if( psz[ i ] == '\0' && ( c == '0' || partial_match ) )
return 1; /* Matched */
if( !isdigit( c = psz[ i++ ] ) )
return 0;
partial_match &= ( c == '2' );
if( psz[ i ] == '\0' && ( c == '0' || partial_match ) )
return 1; /* Matched */
(省略)
if( !isdigit( c = psz[ i++ ] ) )
return 0;
partial_match &= ( c == '9' );
if( psz[ i ] == '\0' && ( c == '0' || partial_match ) )
return 1; /* Matched */
while( isdigit( c = psz[ i++ ] ) ){
if( psz[ i ] == '\0' && c == '0' )
return 1; /* Matched */
}
return 0;
}
--------------------------------------------------
このMatch関数が、求めていたDFAへの変換にほぼ近い内容を表している。状態変化をコメントにしてみると、
--------------------------------------------------
int Match( const char* psz )
{
int i = 0, partial_match = !0;
char c = '\0';
/* DFAの初期状態 */
/* DFAの初期状態が受け入れる文字は数字('0', '1', '2', '3', '4', '5',
'6', '7', '8', '9') */
if( !isdigit( c = psz[ i++ ] ) )
return 0; /* 遷移できなければ終了 */
/* 次のDFA状態に遷移(見えないけど、入力文字が'0'か、それ以外かで遷移先は異なる)
*/
partial_match &= ( c == '0' );
/* '0'で終わっていればマッチしたことになる = 入力文字が'0'だった場合の遷移先には受容フラグ(Accepted)が付いている
*/
if( psz[ i ] == '\0' && ( c == '0' || partial_match ) )
return 1; /* Matched */
/* 次の各DFA状態が受け入れる文字は数字('0', '1', '2', '3', '4', '5',
'6', '7', '8', '9') */
if( !isdigit( c = psz[ i++ ] ) )
return 0; /* 遷移できなければ終了 */
/* 次のDFA状態に遷移(見えないけど、入力文字が'0'か'1'か、それ以外かで遷移先は異なる(partial_matchの値によって遷移先は枝分かれしてゆく))
*/
partial_match &= ( c == '1' );
/* '0'か'1'で終わっていればマッチしたことになる = 入力文字が'0'か'1'だった場合の遷移先には受容フラグ(Accepted)が付いている
*/
if( psz[ i ] == '\0' && ( c == '0' || partial_match ) )
return 1; /* Matched */
(省略)
if( !isdigit( c = psz[ i++ ] ) )
return 0;
partial_match &= ( c == '9' );
if( psz[ i ] == '\0' && ( c == '0' || partial_match ) )
return 1; /* Matched */
while( isdigit( c = psz[ i++ ] ) ){
if( psz[ i ] == '\0' && c == '0' )
return 1; /* Matched */
}
return 0;
}
--------------------------------------------------
1文字読み込んでは次に進み、決して後戻りしないので処理が高速化する。これは簡単な例なので手書きでDFAが実行するコードに近いものを作成できたけど、正規表現なら、3つの基本操作だけを用いて一般的にDFAを作成することができる。この手順(結構、面倒くさい)はアルゴリズムの本なんかに載っているので丸写ししましょう。因みに上で記述したコードは検証してないので間違ってるかもしれないです。
実際には、DFAに変換してしまうとisdigit( )のところが
for( int i = '0' ; i <= '9' ; i++ ){
if( c == i )
...;
}
のようなループになるので本家DFAよりも上のコードの方が速いはず。isdigit(
)とかisascii( )とか、文字の範囲(選択(|))はDFA的には内部ループになるのが痛い。上手く実装すればなんとかなるのだろうけど。
正規表現の基本部分を実装して、最低限の糖衣構文を実装するのだけど、否定(Not)クラス(又は任意の1文字(Any))の取り扱いが面倒。正規表現オブジェクトの入力文字列にはバイナリとエンコーディングが異なる文字列(EUC-JPとか)を指定できるようにし、内部的にはShift_JISかUTF-16(Windowsで言うところのUNICODE)に変換して処理するようにした。1文字進むのが1バイト進むのと同義ではなく、2バイト進むこともあるし、それ以上進むこともある。内部的には最高2バイトになるのでそれはいいとしても、1文字が0x0-0xFFの範囲にないのが問題になった。ある文字の否定クラスを作ろうとすると、0x0から0xFFFFまでの65535に近い数(文字の範囲を調べるのが面倒なので)の構文木を構築しないとならない。
[^0] = \x0|\x1|\x2|\x3|...|\xFFFC|\xFFFE|\xFFFF
DFAじゃなければ簡単に排除できるのだけど、ややこしいことになった。とりあえず、普通にやるとスタックオーバーフローを起こすので末尾最適化してループにし、なおかつ大袈裟に別スレッドを使って正規表現をコンパイルしてみた。[.....](任意の1文字の6回連続)だけで2時間近く待ったけどコンパイルが終わらなかった(デバッグ環境下、遷移表を作るのに時間が掛かりすぎてリリースでも厳しい。因みにメモリは数百メガは使う。もうこの時点で使えないのだが)。C言語で超最適化したとしても数分では終わらないだろうな。バイトに限定するオプションを既定の動作にして諦めた。DFAに変換する正規表現オブジェクトには、ハット(^)とピリオド(.)はあんまり使わない方が良さそう(でも使うけど)。逆にNFAで実装されている場合は使いまくった方が良いという皮肉(逆に閉包(*、+)とか選択(|)に弱いはず)。いずれにしても、有限時間では終わるのですけどね。
頑張って実装してデバッグも終わった。行頭一致(^)と行末一致($)は関数の引数とすることで対処した。どうしても正規表現の文中に埋め込めなかったから。ましてや後方参照とかどうやるんだろうとか思ってたけど、後になって理由がわかった。Perlやその他の実装はNFAエンジンらしい。やられた。NFAなら後方参照とかでも実装は難しくない(と言うか、後方参照とか正規表現ではないのですけど)。実用的にはNFAにして糖衣構文をたくさん使える方がいいらしい。検索対象は常に100MB以上のテキスト、正規表現には否定クラスや任意の1文字を含まない(純粋な正規表現)、という条件ならDFAに変換しないと損だけど、そんな状況って頻繁にあるかと言えば、まずないでしょうね。DFAに変換すると使えなくなるという皮肉、疲れました。
まとめると、DFAに変換するとバックトラックしなくてもよくなるけど、逆にバックトラックしにくくなる(というかほぼできない)。何しろ、決定性なので前にしか進めない。色々なオプションを付けたければNFAで実装するほうがいいかもしれない。というか、NFAの方がいいのでは、という結論に至りました。
左再帰
数学的だか、論理学的だか知らないけど、文法表現が、A = Aaと最終的に変形できる場合を左再帰という。この辺はWiki参照。プログラム的には、再帰降下法では表現通りに実装すると
Node* Hoge( )
{
Node* pNode( Hoge( ) );
if( token == ... ){
...
}
return pNode;
}
という記述になり、無限再帰状態に陥るので変形しないとならない。で等価な表現は、
Expr = NUMBER |
Expr , "+" , NUMBER ;
なら
Expr = NUMBER , Expr' ;
Expr' = ( "+" , NUMBER ) , Expr' |
EPSILON ;
と変形することで等価な表現に変換できるらしい。要は、拡張BNFの
Expr = NUMBER , { "+" , NUMBER }* ;
としてよい。
結合性
定義の加算と代入では、加算が左結合、代入が右結合らしい(この辺がよくわからない)。
AdditiveExpr = MultiplicativeExpr |
AdditiveExpr , "+" , MultiplicativeExpr |
AdditiveExpr , "-" , MultiplicativeExpr
;
AssignmentExpr = ConditionalExpr |
UnaryExpr , ( "=" | "*=" |
"/=" | "%=" | "+=" | "-=" | "<<="
| ">>=" | "&=" | "^=" | "|="
) , AssignmentExpr ;
まとめ(1)
とりあえず、"AdditiveExpr"を左再帰を取り除いた表現で表すと、
(A)
AdditiveExpr = MultiplicativeExpr , AdditiveExpr___ ;
AdditiveExpr___ = ( "+" , MultiplicativeExpr ) , AdditiveExpr___
|
( "-" , MultiplicativeExpr ) , AdditiveExpr___
|
EPSILON ;
となる。更に、拡張BNFの繰り返し表現を使って"AdditiveExpr"を表すと、
(B)
AdditiveExpr = MultiplicativeExpr , { ( "+" | "-")
, MultiplicativeExpr }*
となる。いずれも再帰降下法で実装できる形になった。
左結合
( 1 + 2 + 3 + 4 )が、( ( ( 1 + 2 ) + 3 ) + 4 )という順で評価されるように構文木を構築すればいい。
Node
+
Node , Leaf( 4 )
+
Node , Leaf( 3 )
+
Leaf( 1 ) , Leaf( 2 )
右結合
( 1 + 2 + 3 + 4 )が、( 1 + ( 2 + ( 3 + 4 ) ) )という順で評価されるように構文木を構築すればいい。
Node
+
Leaf( 1 ) , Node
+
Leaf( 2 ) , Node
+
Leaf( 3 ) , Leaf( 4 )
左結合の実装
まとめ(1)の(B)の表現を素直に実装すれば左結合になる。要はループで実装する。
Node* AdditiveExpr( )
{
Node* pNode( MultiplicativeExpr( ) );
GetToken( );
while( token == '+' || token == '-' ){
INT iOp( token == '+' ? OP_PLUS : OP_MINUS );
GetToken( );
pNode = new Node( iOp, pNode, MultiplicativeExpr( ) );
}
return pNode;
}
右結合の実装
まとめ(1)の(A)の表現を素直に実装すれば右結合になる。要は再帰で実装する。
Node* AdditiveExpr____( Node* pNode )
{
if( token == '+' || token == '-' ){
INT iOp( token == '+' ? OP_PLUS : OP_MINUS );
GetToken( );
pNode = AdditiveExpr____( new Node( iOp, pNode, MultiplicativeExpr(
) ) );
}
return pNode;
}
Node* AdditiveExpr( )
{
return AdditiveExpr____( MultiplicativeExpr( ) );
}
同じ構文木を生成するように書き換えると、
Node* AdditiveExpr( )
{
Node* pNode( MultiplicativeExpr( ) );
GetToken( );
if( token == '+' || token == '-' ){
INT iOp( token == '+' ? OP_PLUS : OP_MINUS );
GetToken( );
pNode = new Node( iOp, pNode, AdditiveExpr( ) );
}
return pNode;
}
なんか不思議な(怪しい)感じがしますね。これって右再帰と言えば右再帰な気が。元々、左再帰を左再帰を取り除いた形に変形できたので、この式も右再帰を取り除いた形に変形すれば左再帰に戻る、と考えれば納得なのですが。これを曖昧と言うのかもしれません。結合性が変わってしまうとかWikiに書かれていたけど、逆に変えようと思えば変えられるのかもしれない。とりあえず、再帰降下する場合は、結合性とか既にわかっていてコントロールできるので実装する時にどちらかを選べばいいということで。多分、どっかが間違っているんだろうなと思いつつも、とりあえず、右結合を実装したいだけなので無視します。
まとめ(2)
結合性はBNFで書かれた式の範囲外になり得る(のかもしれない)。yaccに%leftとかあるのも、切り替えが難しくないからなんだろうなとは思った。簡単には、再帰降下する過程で、左結合にしたければループで、右結合にしたければ再帰で実装する。ポイントはこれですな。因みにこの文章が正しいかどうかは保証できません。若い方は学校へ行って教わりましょう。
正規表現の続き。結局、実装を変更することにした。最初は1文字づつ読み込んで(エンコードによって1文字が2バイトの時もあれば、3バイト以上になることもあるけど、内部でShift_JISかUTF-16に変換するので1文字のバイト数は最大で2バイト=65536種類の文字の集合)、その文字が一致するかを判定していた。でもある文字の否定表現を作ると65535個のノードが必要になってしまった(普通の人は最初に思いつくのでしょうけど)。正規表現にマルチバイト文字が含まれていない時はバイトの否定にすることで諦めたけど、やっぱりよろしくないので今度は1バイトづつ読み込んで、マッチしなかったら1文字進む(逆に正規表現文字列の方を入力テキストのエンコードに変換する)という実装にすることにした。これなら漢字の否定表現もできなくはない。で、終わったのだけど今度は1文字のバイト数が何バイトになるかわからなくなってしまった。例えば、”正規表現”の’正’という文字を考えると、Shift_JISとかUTF-16なら2バイトと断定できるけど、他だと何バイトになるかわからない。で、それらの文字の否定表現は、何バイトになるかわからない文字の集合の否定になるので余計に複雑になってしまった。否定表現を表す構文木を構築するクラスを特別に作ってなんとかした。例えば、[^12]で入力テキストがShift_JISなら”最初のバイトが0x82以外|最初のバイトが0x82で次のバイトが0x50か0x51以外”という正規表現を作成する。[^1正]だと”最初のバイトが0x80か0x82以外|最初のバイトが0x82で次のバイトが0x50以外|最初のバイトが0x80で次のバイトが0xB3以外”になる。他のエンコードで1文字が3バイト以上だとかなり複雑になるけど、同じ要領で構築していく。ただ、問題があって(簡単だったらとっくに誰かがやってるでしょ)、例えば、[^12]は"正"の2バイト目にマッチしてしまう。どこで妥協するかがポイントなのだけど、全てのバイトが同じ数値の文字とか考えると頭が痛くなるので、この辺で妥協することにした。
で、左再帰の続きだけど、Wikiとかでは左再帰の実装が難しいみたいに書かれていたので不思議に思っていたけど(複雑な文法なら実際に難しいのですが)、理由がやっとわかった。その文章はHaskelみたいな言語を前提としているらしい。Haskelでは再帰(と呼ぶのかどうか)が普通で、逆にループ(制御構造)がないので簡単な左再帰でも難しく感じるというというのが理由だと思うのですがどうなんでしょ。Haskelは前にちょっとだけかじったけど、ファイル操作やウィンドウ操作を行う時は記述が手続き型言語っぽくなるし、うーむ、と思っていたけど、パーザを作るようになったらHaskelなら楽だよなぁと思うようになった。
プログラムとは関係ないのだけど、OCPickerの実装が一段落したので、休みを取ろうと思った。一段落と言ってもマニュアルとか出来ていないのだが。プログラムがある程度大きくなると、本来的な仕事よりも、エラー処理とかマニュアル、ヘルプ作りの方が大変になる。プログラムの内部状態の1つを2進数で表すと(BOOL値のフラグ)、状態が1つ増えるだけで、状態数の合計は2倍になる。そして、その説明も2倍になる。説明することが増えすぎて、やってられなくなるのだけど(プログラマよりも説明やヘルプを作る方が、高度な仕事だと思う)、まぁ、それでも一段落と言うことで休みを取ることにした。
先日、ラーメン屋で読んだエンジェルハートというマンガが気になったので、残りを読もうとマンガ喫茶へ行った。なんかシティハンターの続き物らしい。普段、テレビを観ない(もう何年観てないのかわからない)ので、アニメ化されていたことも知らなかった。突然、読みたい本が28巻まで出てるかと思うと何か幸せだ。とりあえず、全巻読み終えて休暇を満喫した(もっとまともな楽しみがあればいいのですけどね)。リョウ(常用でないらしいのでカタカナで書きます)がおっさん化していて時代を感じました。所詮はマンガなので疑問を呈していたらキリがないのだけど、主人公の元殺し屋の女の子が飛び降りして死のうとしたのが、まずあり得ないと思いました。殺し屋で確実に死にたければ、口に拳銃を突っ込んで脳味噌に弾丸打ち込んで死のうとするでしょ、普通。生存確率が0には近いが、0ではない方法で死のうとしたのがプロっぽくないですね。ゴルゴは常に眉間を狙いますよ。
YourFileHost.comでダウンロードしたいファイルがあったのだけど、通常の方法ではダウンロードできなかった。まぁ、いいや。他で探そうと頑張ったのだが他では見つからない。有料でも良かったのが、見つからないのではしょうがない。YourFileHost.comの無料プレミアムメンバーに登録すればダウンロードできるらしいのだが、クレジットカードの情報を入力しないとならないので諦めた。捨てアドならぬ捨てクレカでやってもいいのだけど、このサイトの場合、有名らしいけど、怪しすぎるので何かやられた場合に言い訳にもならないし。目の前にある無料のファイルをどうやって取得するかが考えることになった。ウェブ関係とか知識がないのでどうすればよいのかわからないけど、とりあえずFireFoxではなくIEでキャッシュに保存されるか見てみた。swfという形式のファイルでキャッシュされるらしい。で、このswf形式のファイルだけど、どれもサイズが同じで、動画にしては小さかったので、本体はどこか別の場所にあるのだということは感覚的にわかった。
とりあえず、swfを読み込んでaviか何かに変換してくれる方法を検索してみた。AnvsoftのFlash
to Video Converterとか、SwfToAviで変換できるらしい。面倒くさいなと思っていたら、既にディスクに入っていた。昔、困ってダウンロードしたんだろうな、多分。記憶にないけど。で、前述の2本と他の変換ソフトを試してみたけどうまくいかない。swfを読み込んで、その本体をIE経由で取得する時にエラーになるようだ。簡単に変換させないためにswfにしてるんだろうから、変換ソフトが出揃ったところで変換できないような方法に換えちゃうよね、普通。
ページのソースを読むと、どうもドメイン関係でアクセス制御を掛けてるっぽい気がしないでもない(知識ないのでよくわからないけど)。手打ちでファイルが取得できるかやってみた。とりあえず、怪しそうなのは、フラッシュオブジェクトに渡す属性と言うのか”movie”の”video=http...”以下の引数部分。バックスラッシュとか16進表記になっているので手作業で修復して、引数を適当に改竄して、そのサイトにアクセスすると”video_id=http...”という文字列を含む引数リストのようなものを返してきた。そして、その中に見慣れた”.flv”の文字が。もう一回、”video_id=http...”以下の部分をデコードし、そこにアクセスするとお目当てのflvファイルが手に入った(但し、事前に動画を一度は再生しておかないとアクセスが拒否される)。flvなら変換しなくても観れるから悪くないな。
多分、もうやることはないだろうし、書式もその内に変更されるだろうけど、忘れそうなのでメモっておくことに。
クレジットカードで思い出したのが、数ヶ月前にネットショッピングのサイトで何物かに勝手にログインされて買い物されたことがあった。買われたものというのがアダルト動画。しかも、2度に渡り、2回目は前回から数ヶ月経ってからだった。1回当たりのダウンロード数はアダルト動画で4、5本。無期限ダウンロードでディスクに保存できる商品だった。1年くらいログインしてなかったので、商品リストを見た時は、なんでこんなにたくさんのアダルト動画が表示されるんだろと思ったけど、よく見ると購入済みの商品だった。
で、対策を考えることにしたのだけど、前回、購入されたのが数ヶ月前という状態(購入代金とかは既に清算済み、気が付かなかった)、被害も1万円に届かない額なので泣き寝入りすることにした(泣くほどではなくて、むしろ助かったかもしれない)。サイトのパスワードとか関連するパスワードを変更して、それで終わりにした。
サーバーにカード情報を登録していたのがまずかったのだけど、登録していたのはVISAで、購入時にカードのパスワードの入力が要求される奴だったのに、そのパスワードも破られたってことになる。サイトのログインパスワードはともかく、カードのパスワードまで突破するのは至難の技だと思うのだが、どうやったんでしょう。せっかくだから、犯人が購入した動画をダウンロードして観ようと思ったけど、女優が80年代(?)の方々ばかりみたいで、気力が失せた(もう少し新しいのにして欲しかったな、無期限ダウンロード可能なだけに損した気分)。おっさんのエロパワーは凄まじく、両さんみたいだったというお話です。
何か、ブラウザの言語設定で日本語を削除すれば大丈夫らしい。プギャー。
最初に使い出したブラウザはNCSA Mozaic、というか当時はそれしかなかった(HTMLも手入力が当たり前だったと思う。ブラウザすらままならないのだから当然だが)。次にNetscape
Navigator、でInternet Explorerが登場して、何となくOperaの時期もあったかも。でもIEに戻って、重さに我慢しかねてFirefoxを使うようになり、今度はGoogleのChromeに乗り換えた。Sleipnirはインストールしたものの、イマイチ良さがわかっていない。
シンプルさが好きでFirefoxを使うようになったのに人気が出始めたら、どんどん重くなる。それでもIEよりはマシだったのだけど、Chromeの軽さを目の当たりにして乗り換えることにした。操作にはまだ慣れないけど、軽いなぁ。機能も少なくていいな。
織田信長じゃないけど、誰かブラウザ(エンジン部分)の世界で天下統一を果たしてもらいたいのだが、メジャーになった瞬間に脆弱性を狙われるのでどうすることもできないか。Active
Xがあるから、どうしてもIEコンポーネントは生き残る。Operaは独自エンジンで頑張ってるらしい。ある意味、すごいです。他のエンジンに変更したら存在意義がなくなってしまうので、独自開発を続けるしかない。しかし、所詮は多勢に無勢、スポンサーがいなくなれば滅びる運命っぽい。
とか書いてたら、Sleipnirを使う羽目になった。このブラウザ、基本的にIEコンポーネントを利用するのであまり使いたくないのだけど、プロキシの切り替えが簡単という点だけは他のブラウザより優れている。で、中国の動画サイト(youku.com)を閲覧するために、中国の(プロキシ)サーバーを経由したいという時に使っている(日本からのアクセスは拒否されてしまう)。法的問題や規制でyoutube.comが駄目駄目になったから他サイトへの流出がすごい。Googleに買収された時点で誰もが終わったと感じていたと思うけど。まともな企業に買収されると訴訟の対象になるから、弁護士が狙いまくり。それが嫌で中身だけどっかに移動しちゃうんだよね。正にイタチごっこ。
久しぶりにパソコンのプログラム関係のバックアップを取ろうとしたら、Yahoo!ブリーフケースが有料サービスに移行していた。既存のファイルは2009年2月1日付けくらいで全て削除されていた。3ヶ月前か、知らなかった。まぁ、楽天とか他の場所にもミラーリングして保存してるので問題ないし、そもそも全部消えてなくなってしまっても、生きていけないほど困るものでもないし。無料で使わせといて後で梯子を外すのはビジネスの基本なので仕方ないけど、月々数百円の安定収入が目標になっている時点でYahoo!も斜陽企業になってしまった感は否めない。こういう企業の株はどんどんプレミアムが剥がれていくから買いたくないな。
C言語を使うようになった理由を思い出してみた。当初はプログラマになりたかったので、とりあえずC言語というのがあった。その当時は、メインマシンがLinuxで、他Macとかだったので、まぁ、Linuxと言えばC言語が基準になるのも当然だった。Visual Studioが今日のように無料で手に入る時代ではなかったので、コンパイラとか無料で使えるLinux環境は魅力的だった。で、入門書から始めて、ポインタで躓いて、アセンブラとかスタックマシンの仕組みを学ぶうちに慣れてきて(アセンブラがわかんないとC言語の理解はなかなか難しいかも)、Linuxのシステムコールがどのように実行されるか、gdbとかで追うようになって(gdbで追えるのは実際にはCライブラリの同名の関数なのだが、内部的には引数を整えてINT0x80でカーネル領域に飛んでいた)、Intelマニュアルを読むようになって(Intelのサイトで手に入る文書を読むだけでx86アーキテクチャの基本部分はだいたいわかる)、まあ、ここまでが基本部分。
その後、Windowsに移行するとちょっとカルチャーショックを受ける。システムコール(Windows
API)が多いこと(まぁ、カーネルの実装の仕組みが違うのだが)、fork( )&execve(
)がCreateProcess( )になり(これは良い変化かも)、ファイルディスクリプタ関連がややこしくなり(WindowsでもソケットをReadFile(
)、WriteFile( )で読み書きできるけど、この辺はLinuxの方が良かったかな)、スレッドや非同期処理が充実していることに目を見張る(Windowsはスレッドが使いやすいので使わないと損だと思ってしまう)。
その内にWindows APIに慣れてきて、Windowsの駆動の仕組みを学ぶようになり(MFCを使わないで生Cで解説している本を探しまくった記憶が)、ダブルバッファリングとか基本的なテクニックを学び始め、OLE(Active
X)の仕組みを学ぶようになって、C++に移行(C言語でも記述できなくはないけど、IUnknownインターフェイスだけのオブジェクトを作成するだけでも結構大変)。C++でライブラリがある程度充実すると、思いついたことは、ほぼプログラムできるようになる。高等教育を受けていないのでアルゴリズムとかには弱いのだが、現実のプログラムは他の人のコードをコピペするだけで仕上がるようなものがほとんどなので。最終的にはプログラムに対する興味を失うのですが(笑)。
最後にプログラムを始めた頃に書いていたお遊びコードを。こういうコードはC言語でないとなかなか書けない。
#include <stdio.h>
#include <stdlib.h>
static void malicious_code( )
{
fprintf( stderr, "The return address was overwritten.\n" );
exit( -1 );
}
int main( int argc, char** argv )
{
static int i;
void* buf[ 1];
for( i = 0 ; i < 10 ; i++ )
buf[ i ] = malicious_code;
return 0;
}
前々から新しい言語に挑戦したいとは思っているのだけど、C言語から抜け出せないでいた。もうちょっと生産性の高い楽な言語がいいなと思っていたので、色々試してみた。個人的にはDelphiみたいなウィンドウプログラムが記述できてネイティブコードを吐く言語が希望だったので、とりあえず同じVCLを使っているTurboC++をインストールしてみた。結果はイマイチ。仕方ないのでネイティブじゃないけどJAVA.。エクリプスとNetBeansをインストールしてみた。ライブラリが充実しているのでいいけど、新しいライブラリを覚えるのもなんだなと思い、結局は.NETに。Visual
BasicやC#は前に試したことがあってイマイチだったので、今度はC++/CLIを試してみた。Visual
C++ 2003の頃は文法も流動的だったし、ビジュアルにフォームを配置できなかったけど、Visual
Studio 2008ではできるようになっていた。気に入ったので移行しようと思った。詳しいことは判らないけど、ネイティブと非ネイティブが混在していてもいい点が楽だし、かつ文法もあんまり、覚えなくても良さそう。C++/CLIの凄いところは下のコードのようにマクロ使えるんですよね。Visual
Basicとかでは考えられない。
#include "windows.h"
#include "vcclr.h"
#using <System.dll>
#using <System.Windows.Forms.dll>
using namespace System;
#define ___MESSAGE1( msg ) System::Windows::Forms::MessageBox::Show( msg, L"Library" )
static INT ___MESSAGE2( String ^s )
{
pin_ptr< CONST Char > ps( ::PtrToStringChars( s ) );
return ::MessageBoxW( NULL, reinterpret_cast< LPCWSTR >( ps ), L"Windows API", MB_TOPMOST );
}
INT main( array< System::String ^ > ^args )
{
Console::WriteLine( L"文字列を入力して下さい。" );
String ^s = Console::ReadLine( );
___MESSAGE1( s );
___MESSAGE2( s ); // MB_TOPMOSTを指定。
return 0;
}
テストコードでいきなり困ったのはMessageBox::Show( )にMB_TOPMOSTフラグとかを指定できないらしい点。有り得んなと思いつつも、C++/CLIならAPI呼び出すのも簡単。とりあえず、String型をC言語の文字列型に変換する方法を調べてみた。KB(http://support.microsoft.com/kb/311259/ja)によれば、”vcclr.h”のPtrToStringChars関数で文字列へのポインタを取得できる。その際に、GCとから勝手にメモリを移動しないようにpin_ptr<wchar_t>とかで固定しておくだけ。PtrToStringChars関数は何をやってるかと言えば、オブジェクトの先頭アドレスから文字列データへのオフセットを計算してるだけ。文字列データ自体は、C言語形式(但し、UNICODE)のデータ配列として保存されているらしい。それ以外の方法は文字列のコピーを作成することになるようだ。商用バージョンには”msclr/marshal.h”というヘッダが存在してmarshal_as関数で楽にできるようなのだがExpressにはない(どうしてもというなら、Microsoftから90日評価版のプロフェッショナルをインストールして参考にはできるけど).。
言語としては最強なのではと思うのだが、人気はないだろうなぁ。
新しい言語の続きで、折角なのでVisual Studio 2008 Professional 評価版もインストールしてみた。ExpressではリソースエディタとMFCが提供されないけど、それは個人的に問題ではなかった(Visual
C++ 2003 Standardは購入したので、最悪の場合は2003で処理できた)。でも、”msclr/marshal.h”は一度見ておかねばと思った。内部でやってることが大したことなければ、自分で似たようなのを作ればいいし。MSからISOイメージをダウンロードしてインストールが完了したら、”msclr/marshal.h”も無事インストールされた。1日仕事だった(疲れた)。
評価版の日付チェックはインストール日を基準にしてるらしいので、パソコンの内部時計を十分に進めておくと期限が延びるらしい。また、起動の瞬間に内部時計をインストール日の数日後に変更して、起動後に元に戻すというプログラムも存在するみたい(この手のツール作成は得意なんですけどね)。しかし、何よりも評価版の制限を外すシリアルがネット上に氾濫してるのはスゴイなと思った。下手なシェアウェアの方が違法コピーに耐性があるのではと思ってしまう。一応、C#はインストールしたけどVBはしなかった。
で、”msclr::marshal.h”の内容的にはどうかというと(パッと見ただけなのですけど)、Stringから文字列へのポインタ(所謂、C言語型の文字列)に変換するにはPtrToStringChars関数を使い、その後でメモリを固定するという上記の___MESSAGE2(
)とほぼ同じ操作を行っているけど、メモリを確保して文字列の複製を作るため(context_nodeクラス)遅いだろうことがわかった。更にWideCharToMultiByte(
)などのAPIを読んだり、内部的にオブジェクトを生成したりとかかなりの手間を掛けているので、___MESSAGE2(
)より遥かに遅くなるはず。その代わりに汎用性はあるかな(なかったら意味ないし)。
-----MSDNより引用始め-----
マーシャリングでコンテキストが必要となるのは、マネージ データ型からネイティブ データ型へのマーシャリングで、変換先のネイティブ型に自動クリーンアップを行うためのデストラクタがない場合だけです。マーシャリング コンテキストは、割り当てられたネイティブ データ型をそのデストラクタで破棄します。したがって、コンテキストを必要とする変換は、コンテキストが削除されると無効になります。マーシャリングされた値を保存するには、独自の変数に値をコピーする必要があります。
-----引用終わり-----
要はマネージ型をネイティブ型に変換する時は、基本的にコンテキストが必要。コンテキストを破壊するとネイティブ型への変換に利用されたメモリとかが解放される(コンテキストの生存してる間は返されたネイティブ型は有効でメモリを手動で解放する必要もない)。理屈的に(コード的に)marshal_as(
)は相当に遅いっぽいので確認してみる。簡単にGetTickCount( )で同様の変換処理を行った時にどれくらい差がつくかテストしてみた。テストコードは以下です。
--------------------------------------------------
#include <windows.h>
#include <msclr\marshal.h>
using namespace System::Diagnostics;
using namespace msclr::interop;
[STAThreadAttribute]
INT main( array< System::String^ >^ args )
{
String^ s( L"abcdefghijklmnopqrstuvwxwz" );
LPCWSTR pszW( NULL );
INT iTick[ 3 ] = { 0, 0, 0 };
CONST INT c_iMaxLoop( 2500000 );
iTick[ 0 ]= ::GetTickCount( );
// marshal_as( )を使った実装(内部でコピーを作成する)。
{
marshal_context context;
for( INT j( 0 ) ; j < c_iMaxLoop ; j++ ){
pszW = context.marshal_as< LPCWSTR >( s );
}
}
iTick[ 1 ]= ::GetTickCount( );
// シンプルな(コピーを伴わない)実装。
{
for( INT j( 0 ) ; j < c_iMaxLoop ; j++ ){
pin_ptr< CONST Char > ps( ::PtrToStringChars( s ) );
pszW = reinterpret_cast< LPCWSTR >( ps );
}
}
iTick[ 2 ]= ::GetTickCount( );
// ティックカウントを調べる。
s = String::Format(
L"marshal_as( ):\t{0}秒\r\n"
L"my_as( ):\t\t{1}秒",
( iTick[ 1 ] - iTick[ 0 ] ) / 1000.0,
( iTick[ 2 ] - iTick[ 1 ] ) / 1000.0 );
Debug::WriteLine( s );
return 0;
}
--------------------------------------------------
結果は、
marshal_as( ): 41.609秒
my_as( ): 0.063秒
と、GetTickCount( )の簡易テスト(デバッグ環境&途中でDLLの遅延ロード等もあるかも)とは言え、100倍以上の速度差がついてしまいました。前者はループのたびにコンテキスト内に変換に利用したメモリが蓄積されていくのに対して、後者はほぼスタックのみで実行可能なため、ループ数が増えれば増えるほど差は更に広がると思われます。また、頻繁な呼び出しに対してはmarshal_context生成のオーバーヘッドが馬鹿にならなくなり、結論として汎用性のために速度はかなり犠牲にしてることがわかりました。
3倍くらいは差が付くかとは思ったけど、すごい差だ。頻繁に利用しないのなら、汎用性を考えてmarshal_as(
)を使うべきだとは思うけど。反対のC言語の文字列からStringへの変換は、素直にmarshal_as(
)を使った方がいいとは思った。
OpenProcess( )がNULL(GetLastError( ) = 5, アクセス拒否)を返して失敗する。少し昔のプログラムをコンパイルし直したのだが、純粋なAPI呼び出しに失敗する。インクリメンタルリンカの影響とか、VSでよくある不具合かと思いプロジェクトをリビルトしてみたけど駄目。仕方ないのでOpenProcess(
)の1行のみのWinMain( )を記述して実行してみたけど、それでも失敗する。加えて、プロセス系のAPIの挙動もおかしい。「あり得ーん」と思いつつ、「OpenProcess
失敗」、「OpenProcess fails」でGoogle先生に聞く。日本語文書は見つからなかったけど、米系のサイトで同じような問題が発生していた(日本でも困っている人はいるはずなのだが、いつも思うけど見つかりにくい)。で、見つかった答えは(http://msdn.microsoft.com/en-us/library/ms684880(VS.85).aspx)。VS2008のPlatform SDKでPROCESS_ALL_ACCESSSの内容が変わったらしい。とりあえず、
#define _WIN32_WINNT _WIN32_WINNT_WINXP
とすれば昔の動作に戻るらしい。調子に乗ってVS2008 Professionalをインストールしたのがそもそもの原因でした。内容も良くわからないようなオブジェクトならともかく、単純なAPI呼び出しに失敗して原因がわからないと焦ります。
IDA interactive disassemblerというソフトがあります。私は怪しそうなファイルが何をするかちょっと確認したい時などに利用していますが、コンピュータウィルスの解析などでは定番のソフトらしいです。利用頻度は多くないので特にアップデートとか気にしていなかったのですが、そのIDAが今は無料で配布されているのを知って驚きました。こんなに素晴らしいソフトが無料で手に入るとは、あり得ーん(笑)。すかさず、フリー版をダウンロードしてみました。全然問題なく使えました。
プログラムからヘルプファイルの実行ができない(idahelp.hlpというファイルが欠如してため。有償版にはidahelp.hlpが存在する。IDA.HLPとは異なる)ですが、idahelp.chmという別のフォーマットのヘルプファイルが同じディレクトリにあるので、そちらを使えばOKです。内容も同じです。こちらのヘルプ形式はブラウザコントロールを利用しているため、マウスの戻るボタン(MSのインテリマウスなどに付いてる)が利用できて便利です。
Visual Studio 2008の既定のプロジェクト設定
今時ですが、文字コードはMBCS、Cライブラリは常にスタティックなものを利用しています。毎回、プロジェクトのプロパティを変更するのが億劫になってきたので元ファイルを変更することにしました。幸いにVisual
C++ 2003の環境がありますので、そのファイルの状態に戻してあげればいいです。設定ファイルはどこかというと、VS2008では、”C:\Program
Files\Microsoft Visual Studio 9.0\VC\VCWizards\AppWiz\Generic\Application\scripts\1041\default.js”になります。このファイルをVS2003のファイルと比較して違うところを書き直します。違いはWinDiffなどで調べます。

CLTool.RuntimeLibraryプロパティを設定するコードが変更されているのが分かります(VS2003の頃(黄色の部分)には今は利用できないrtSingleThreadedの記述があります)。キャラクターコードの設定などもこのファイルで行われています(個人的には、余計なことばかりしやがってと思うのですが)。因みに関数名はAddSpecificConfig(
)で、まさにカスタマイズするためにあるようです。MSDNを参考にオブジェクトブラウザに.NETコンポーネント(Microsoft.VisualStudio.VCProject、Microsoft.VisualStudio.VCProjectEngine)を追加し、どんな値が設定可能かだいたい調べてスクリプトを書き換えます。私の場合はMBCS、スタティックライブラリ、/EHaだけですので以下の行を追加することで変更できました。変更する際は念のためにバックアップを取って下さいね。
// libc6による追加コードです。
{
var config = proj.Object.Configurations.Item("Release");
config.CharacterSet = charSetMBCS;
var CLTool = config.Tools("VCCLCompilerTool");
CLTool.RuntimeLibrary = rtMultiThreaded;
CLTool.ExceptionHandling = cppExceptionHandlingYesWithSEH;
}
{
var config = proj.Object.Configurations.Item("Debug");
config.CharacterSet = charSetMBCS;
var CLTool = config.Tools("VCCLCompilerTool");
CLTool.RuntimeLibrary = rtMultiThreadedDebug;
CLTool.ExceptionHandling = cppExceptionHandlingYesWithSEH;
}
0xDEADBEEF、デバッガで何か調べていた時に見つけた記憶があるのだが、偶然にしては不自然すぎるので、多分、デバッグ版ライブラリの書き換えチェック用のフラグだろうということで納得していたのだが、ふと気になって調べてみた。やはり、デバッガ用のマジックナンバーとして使われることが多いらしい。まぁ、真相はどうでもよいのだが。ちなみに、0xCAFEBABEはJavaのマジックナンバーらしい。そう言えば、Javaの開発チームの人たちがよく利用していたカフェがあって、それがどうたらこうたらという話を読んだ気もするけど、忘れた。他にも0x0BADF00D、0xFEEDFACEとかあるようだ。普通(x86)の32ビットパソコンではレジスタ長が4バイトなので、マジックナンバーも4バイトが適切かも。
全く意味はないのですが、せっかくの機会なので、Yahoo!辞書(新グローバル英和辞典)に載っている(AからFまでの6文字と0(Oの代用)を使って構成される)4文字の単語をピックアップしてみました。結果は以下です。
ABBA
ABBE
ABCC
ABED
ACAD
ACDC
ACED
AFDC
BABA
BABE
BADE
BBBC
BBFC
BEAB
BEAD
BEDE
BEEB
BEEF
BOAC
BODE
BOFF
BOOB
CAAC
CAFE
CAFF
CEDE
COBB
COCA
COCO
CODA
CODE
COED
COFC
COFE
DACE
DADA
DADO
DAFF
DEAD
DEAF
DECA
DEED
DODO
DOFF
ECAD
EDDA
EEOC
FACE
FADE
FADO
FAFF
FEEB
FEED
FOAF
FOOD
OBOE
OECD
個人的な感覚で簡単な単語をピックアップすると、
BABE
BOOB
CAFE
CODE
DEAD
DEAF
DEED
FACE
FADE
FEED
FOOD
あたりですかね。意外と少ないです。検索に利用したJScriptはこれです。7種類の文字から構成される4文字の単語を単純に全数検索するので、7の4乗=2401ページを読み込みます。1ページの読み込みに0.5秒かかるとすると、2401*0.5秒=約1200秒=約20分かかります。もし、8文字の単語を調べるとすると、7の8乗ですので5764801ページ、5764801*0.5秒=約2882400秒=約48040分=約800時間=約33日かかることになります。
データ量的には、"aaaa"を検索して失敗した場合にコンテンツで18407バイトのデータを受信していました。HTTPヘッダとかその他のことも考えて約20KBとすると、5764801ページでは、115296020KB=約112593MB=約110GB前後のはずです。データ量的には、1日でなんとかなる量です。
C++言語でマルチスレッド(確か1プロセスで最高64スレッドまでは作れたと思った。今はもっと増えてるかもしれないですけど)で書いて頑張れば数日で終わるかもしれない。ただ、Yahoo! JAPANの1日のページビューは10億以上あるっぽいですが、メインページでなく検索が必要なページを500万ページ1人で読み込むのですから目立つでしょうね。
暇つぶしに宣教師の川渡り問題とか言われるらしい問題を全数検索で最短手順を求めるJScript(これ)を書いてみた。コードの速さはそれほど求めていないので非効率ですが、宣教師5人、先住民5人、ボートに乗れる人数3人が解ければ現実問題として十分ですので。
コードを書くと色々わかるのですが、初期状態で左岸の宣教師の人数は人喰いの人数以上であり、かつ、ボートに乗れる人数が4人以上だと、宣教師と人喰いが2人ずつ渡って、1人ずつ戻るパターンを繰り返せば必ず解けてしまうので、問題としてボートに乗れる人数の最大値は2人か3人に限定されます。ボートに乗れる人数が3人の場合は、初期状態の左岸の宣教師の人数が人喰いの人数よりも多ければ、宣教師2人と人喰い1人が渡って、宣教師1人が戻るパターンを、ボートに乗れる人数が2人の場合は、宣教師1人と人喰い1人が渡って、人喰い1人が戻り、宣教師1人と人喰い1人が渡って、宣教師1人が戻るパターンを繰り返せば解けてしまうので、問題として初期状態の宣教師と先住民の人数は等しい必要があります。更に、ボートに乗れる人数が初期状態の左岸の宣教師の人数(=人食いの人数)以上だと、一方が全員渡って1人戻り、他方が全員渡って1人戻り、最後に1人ずつ渡ればいいので、左岸の初期状態の宣教師の数はボートに乗れる人数よりも多い必要があります。これだけ条件が限定されると、宣教師3人、人喰い3人、ボート2人までか、宣教師5人、人喰い5人、ボート3人までくらいしか、まともな問題にならないことがわかります。
因みに、各場合の最短手順と解の数は以下になりました。宣教師3人、人喰い3人、ボート2人までの場合、最短手順のルートが4種類あることを意味します。問題としては、宣教師6人、人喰い6人、ボート3人までに解がないことを証明する方が難易度が高いかも?。
宣教師3人、人喰い3人、ボート2人まで
最短手順: 11、解の数: 4
宣教師4人、人喰い4人、ボート2人まで
解なし
宣教師5人、人喰い5人、ボート2人まで
解なし
...
宣教師4人、人喰い4人、ボート3人まで
最短手順: 9、解の数: 32
宣教師5人、人喰い5人、ボート3人まで
最短手順: 11、解の数: 25
宣教師6人、人喰い6人、ボート3人まで
解なし
宣教師7人、人喰い7人、ボート3人まで
解なし
...
プログラムに頼らずに解く場合は、とりあえず、人喰いのみを右岸にボートに乗れる最大人数になるまで渡らせます。その状態になったら宣教師のみをボートに乗れる最大人数だけ渡らせ、左岸と右岸が釣り合う様にします。宣教師と人喰いを1人ずつ左岸に戻し、宣教師のみを全員右岸に渡らせます。ここで宣教師全員がボートに乗れないと、この方法では解けないです。宣教師が全員右岸に渡ってしまえば、後は適当に人食いを右岸に移動させるだけです。条件的に、初期状態の宣教師又は人喰いの人数がボートに乗れる最大人数の2倍以上だと解けなそうです。
普段から、複雑なクラスとか使わない(多重継承とかpublic以外の継承とか。今のところ、そうする必要性がないからかも)ので、キャストもC言語形式の奴しか使いませんでした。私が書くプログラムは小さなWindowsプログラムがほとんどで、高い割合でインラインアセンブラか機械語を使うので安全なキャストなんて気にする方が阿保らしく思っていました。コーリングコンベンションとかも結構変えるし(ecxレジスタにthisポインタを設定して__stdcallから__thiscallへとかは簡単)、怪しそうなキャストはアセンブラコードを見て確認するし、普段使うライブラリがMFCのようにランタイム型情報を持っているのでdynamic_castしなくても、実行時に上位クラスのポインタがどの下位クラスのオブジェクトを指しているかわかるからです。
特に必要性も感じなかったのですが、ある時、書くことがないだろうと思っていたCOMプログラムを書いていて、どうしてもインターフェイスを多重継承させた方が楽そうだったので多重継承させてみました(ライブラリが多重継承を想定していないので間違った使い方なのですが)。さて、クラスを作成して、そのクラスへのポインタを引数にある関数を呼び出すとアクセス例外が発生してしまいました。ポインタが適切な仮想関数テーブルの位置を指し示していないからなのですが、「親クラスへのポインタには暗黙で変換してくれるのではなかったけ?」と思いつつ、C言語形式でキャスト。この時、コンパイラが仮想関数テーブルへのポインタをずらすようなコードを生成してくれるのか、ただポインタを無理やり親クラスへのポインタに変換するだけなのか(Cならこのキャストになるはずですが、C++では違うはず)、C形式のキャストでは曖昧に思えました。で、初めてC++形式のキャストを色々と試すことになりました。結局は上手くいかなかったのですが。とりあえず、C++形式のキャストに興味を持つ動機にはなりました。プログラムが動けばいいので、インタフェースの仮想関数テーブルが一番上に配置されるようにクラスの宣言順序を変更して動かしました。
COMなのでライブラリのクラスを基本クラスとしない多重継承を目的としたCOM用のクラスを作成するのが良さそうです。オフセットを利用して、void*型でメモリを走査して自力で目的の仮想関数テーブルへのポインタを取得して関数に渡すこともできますが、これでは何のためのC++かわからなくなってしまいそうです。今までは、Better
C with string classくらいにしか使っていなかったので、もう少し、C++っぽく使うようにしようと思うようになりました。で、これからはC++形式のキャストしか使わないことに決めました。で、簡単に使い方をまとめてみた(間違っている可能性大なので、信用しないで下さいね)。const_castとdynamic_castはわかりやすくて、static_castとreinterpret_castで混乱しました。C言語の人にはMSDNになるRobert
Schmidt氏が書いたコラム(Deep C++、演算:static_cast(http://msdn.microsoft.com/ja-jp/library/cc440191(VS.71).aspx))がわかりやすいかも。
const_cast:
const修飾子を外すだけ。他のキャストはconstを付けることはできても外すことはできないので、constを外す時はこれを使うしかない。volatileとか、他のいくつかの修飾子も外せるようですけど、volatileは普通外さないのでは(?)。
dynamic_cast:
実行時に上位クラスのポインタから下位クラスのポインタに安全にキャストしたい時に使う。キャストできない場合は0(NULL)が返る。参照型でキャストした場合は、できなければ例外が飛ぶのでやらない方がいいかも。安全にキャストというのは、ポインタが実際に変換先のクラスのインスタンスか、そのクラスを継承したクラスのインスタンスを指していて、クラスのどのメソッド(メンバ関数)を呼び出しても大丈夫なこと(かな?)。実行時型情報(RTTI)を利用するので、コンパイラで型情報生成の機能をオフにした場合は使えない。実行時型情報は、簡単には家系図のようにクラスの上下関係(継承関係)を記録した部分データベースで、このデータベースにアクセスすると、自身がどのクラスのインスタンスかがわかる。ついでに上位クラスもわかるので、家系図を上に辿っていって、変換元と変換後のクラスが安全にキャストできる関係にあればポインタを返すという仕組み。下位クラスはわからないはず(わかったらMFCのCObjectとか、スタティックライブラリやDLLの中にあるのに、それに付け足すのはできると思うけど、普通やらないでしょう)。海外のサイトとかでRTTIの構造(VC++では、基本的に仮想関数テーブルの先頭アドレス-sizeof(void*)の位置にRTTIへのアクセスするためのポインタが置かれている。コンパイラオプションの関係でRTTIがないDLLとかもある)とか解説されてますので、そちらを参考にして下さい。
RTTIにアクセスしてキャストできるか判断するという仕組みなので、試してないけど、オブジェクトを指し示していないポインタを無理やり渡すと多分、落ちると思う。
reinterpret_cast:
再解釈キャスト。数値からポインタへの変換や、その逆を行いたい場合や、void*ポインタでないポインタから全く関係ない(継承関係になかったり、暗黙に変換できない)他のポインタに変換したい時に使う。ちょっと古いですが、例えば、GetWindowLong(hWnd,
GWL_WNDPROC)でウィンドウプロシージャのアドレスを数値型(long)で取得した後、WNDPROC型に変換したい場合など、C++的には危険なキャストですが、Windows的には全く問題ないキャストなどで使うためにある思います。
特徴はビット配列を全く変更しない。だから、C言語で書く*(DWORD*)&fooみたいな使い方をする時にも使う。C++のキャストは長いので欝になるけど、*reinterpret_cast<DWORD*>(&foo)は、見栄えがいいなと思いました。当然、ポインタからポインタへの変換を何百回、何千回繰り返した後に元の変数に代入しても問題なく動作する(ポインタのサイズは一般にレジスタ長で変換前と変換後で等しく、ビット配列を変更しないので値は変わらない)。
static_cast:
その他のキャスト。数値から数値への変換(整数型から浮動小数点型とかその逆とか)とか、void*型から他のポインタ型への変換とか、継承関係にあるクラスの上位から下位クラスへのキャスト(但し、安全かどうかのチェックは行わない。プログラマの裁量に任されている)。他は、MFCのCStringのようにoperator
LPCTSTR() const(キャストオペレータ)を実装する場合は、CStringからLPCTSTRにキャストできる。変換後のクラスに変換元のオブジェクトを引数に取るコンストラクタ(変換コンストラクタ)があればキャストできる。
基本的にコンパイル時にエラーとかチェックするのでstaticらしい。void*型から他のポインタ型へ変換できるので、Windowsであれば、HANDLEはvoid*で、DECLARE_HANDLEで定義されたHICONなどはint型のメンバを1つだけ持つ構造体へのポインタなので、その変換に使える。例えば、static_cast<HICON>(LoadImage(...))で、LoadImage()はHANDLEを返すのでキャストできる。これは、reinterpret_cast<HICON>(LoadImage(...))でも書けるけど、HGDIOBJとか他の基本ハンドルもvoid*で定義されているようなのでstatic_castでいいかなぁと思いました。
ちょっとスクリーンキャプチャをしようと思ったら、なんか変な画像が出てきてできなかった。Terutenとかいう会社のDRM保護プログラムが動作しているらしい。調べてみたら、Yahoo!コミックを表示する際にインストールされたActiveXオブジェクトが関係しているらしい。まぁ、Yahoo!コミックの表示していなければキャプチャできるので問題はなかったのだけど、仕組みが気になったのでもうちょっと調べてみた。ウェブの情報ではAPIをフックしてるらしい。いつも使っているキャプチャーソフトが使えないので、他のをインストールするのが面倒なので自作した。APIをフックしてるのなら、gdi32.dllとuser32.dllを別名(gdiXX.dll、userXX.dll)でコピーして、GetProcAddress()でAPIを取得して実行すれば大丈夫じゃないかと思ってやってみた。一部のAPIはアドレスの関係でうまく動作しなかったけど(既定のアドレスにロードされないのでエラーが発生しているのだと思う)、うまく動作しなかったAPIはWebShellを起動する前に実行することで回避することにした。で、結果は上手くいかない。
WM_PAINT:
...
pfnBitBlt(hCompatibleDC, 0, 0, cx cy, hDC, 0, 0, SRCCOPY)
pfnBitBlt(hDC, 0, 0, cx cy, hCompatibleDC, 0, 0, SRCCOPY)
と、BitBlt()でhDCからhCompatibleDCに、そして元に戻す操作を行うと変な画像が表示されてしまう。デバッガで調べても、各APIのアドレスの開始位置からコードが書き換えられている様子もない。「おかしいな、APIのフックならこれでかわせるはずだが」と思って、会社の情報から調べたら、フックはフックでもカーネルフックらしい。感じとしては、BitBlt()で元のデバイスコンテキストが表示されているウィンドウの物だと変な画像を表示させるようにしているようだ。実際にインストールフォルダを調べたら、XXX.sysとかあるし。「ユーザーフックでなければ、ActiveXのフックを実行するコード部分を書き換えた方が楽だよな」と思ってやってみたら驚くほど簡単だった。ジャンプ命令を1箇所書き換えるだけで十分だった(こんな手が通用する商用ソフトが未だにあるとは)。それにしても、この会社、世界最高水準のセキュリティ会社という割にDLLは暗号化も難読化もしていないし、手掛かりになるような情報をログに出力しているし、全然セキュアじゃなかった。
習い始めたころは躓いた経験があるのですが、何が難しかったのか思い出せない。今は「ポインタ=星の数合わせ」くらいにしか思っていないし、ポインタの意味とか真剣に理解しようとか思っていないからかも。
int i = 0;
int *pi1 = &i;
int **pi2 = &pi1;
int ***pi3 = &pi2;
...
int **********pi10 = &pi9;
...
int ********************pi20 = &pi19
printf("i=%d\n", i);
printf("**********pi10=%d\n", **********pi10);
********************pi20 = 1;
printf("i=%d\n", i);
Parsecライブラリのインストール(GHC 6.12.1)
趣味でプログラミングしてるので、特にJavaとかC#とかPHPとか仕事で使いそうな言語を学ぶ気がしない。プログラマなら、C++とJava、Perl、Ruby、Pythonの内の1つ、Lisp系の何か1つくらいは最低でも使えるようにしたいものだけど、C++以外は、Javaは文法はわかるけど(結局、ライブラリの使い方覚えるほうが重要だと思うし)、LL系は便利だなと思うけど、使う目的がないし、LispはLinux(Emacs)環境ないので、あんまり覚える気がしない。Windows
Onlyだとコマンドベースの言語はあんまり便利だと思えない。で、JScriptとかVBAとか覚えたけど、特に使う機会もない。F#も文法一通りやったけど、C#に関数型ライブラリを実装した方がいいんじゃないかと思う程度。で、Haskellに。結構、はまった。文字列がリストでアクセスできるのがいいのと、ロジックの組み立てが簡単で気に入って使うようになった。Haskellに慣れてしまうとC++には戻れない。F#に戻ると、すごくじれったい感じがする。ただ、Haskellが役に立つ言語かというと、あんまり役に立たない。例えば、Code
Jamの問題を解く時など、ある程度の量と速さが求められるプログラムだと(まぁ、問題自体がC++を対象にしてる感があるけど)、ロジックを書いてもスピードが出せず、結局、Cチックな配列やビット演算で処理するとかしないとならない。しかも、参照透過性のために配列操作は苦手でかつ面倒、実装できたとしてもC++で書いた方がマシかと思うようなものになってしまう。スピードを求めると、結局、アセンブラ命令とメモリ配置に近いデータ構造に漸近していくだけなので仕方ないのだが。普及しなくていいから、生き残って欲しいなぁとは思う。
で、Windows上のGHC 6.12.1にParsecライブラリが同梱されなくなったようなのでインストールした方法を記録しておく。cabal.exeというプログラムがWindowsだと上手く動作しないようなので、HackageDBからCabal用のパッケージを手動でダウンロード。Parsecはmtlというライブラリに依存してるので同じくダウンロード。ダウンロードしたら、適当な場所に展開して、runhaskell.exe
Setup.hs configure、build、installを実行するだけ。WindowsだとarがないのでCygwinなどのコマンドシェルから実行しないとmtlはビルドできなかった。Windowsにarがないのに、arなんか使うなんて、本体が一生懸命ポータブルにしてるのにアホかと思った。
Visual StudioでHaskellを扱えるようにするコンポーネンントモジュール。関数名がIntellisenseで表示されるのは感動ものだが、既に開発が停滞している感じで利用しているGHCのバージョンは古い。でも、Windows環境だと、Visual
Studioが使えるというメリットは大きい。今まで秀丸で予約語とか自分で登録して使っていたけど、これを使うかもしれない。
あと、よく見てみたらMinGWのbinディレクトリにarはあった。パスが通ってないだけでアホなのは私だった。バッチファイルで一時的にパスを追加するようにしてCygwinなくともビルドできるようになった。Cygwinを使う場合、ディレクトリがUnix形式(c:/ghc/..、Unixにはドライブはないけど...)でなく、MS-DOS形式(C:\ghc¥..)だと失敗することがあるのでCygwinは経由しない方がいいかもしれない、あと、ついでにText.Regexもインストールした。こちらも面倒で、"regex.h"がないと言われて、"http://gnuwin32.sourceforge.net/packages.html"のRegexモジュールのソースコードを入手して、ライブラリを作成した。ライブラリ作成の際は、"HsRegexPosixConfig.h"がないと言われ、"http://vir.mskhug.ru/browser/ghc/ghc-6.6.1/libraries/regex-posix/include/HsRegexPosixConfig.h?rev=2"からコピペして対応した(一部、MAKEFILEも書き換える必要があるかも)。継ぎ接ぎだらけのコードだが、なんとかコンパイルできた。こんなにWindowsユーザーを大切にしない言語も珍しいなぁとは思った。
Text.Regexライブラリのインストール(GHC 6.12.1)
結局、試行錯誤してインストールしたText.Regexライブラリだが、実行しようとするとエラーが発生することがわかった。unknown symbol "_regerror"...と表示される。おかしいな、スタティックライブラリにしたから依存関係などないはずだが...とは思ったけど、WindowsとUnixの境界の話なので自信がない。でも、明らかに動的に解決している感じ。Windowsで言えば、LoadLibaray()が失敗したとか、GetProcAddress()がNULLを返したみたいな状態。とりあえず、Googleで調べて、cabalファイルのExtra-librariesみたいな項目に対象のライブラリファイルを指定すれば解決できそうだったので、試したらなんとか解決した。スタティックではなくRegexのDLLライブラリを使うことになってしまったけど、とりあえずまともに動くのでスタティックな奴を作成する気力も失せた。結局、ダウンロードしたライブラリとDLLを適当な場所にコピーして、適当に変名して、適当にパスを通すことでインストール可能なことがわかった。正規表現ライブラリのくせに何故かポータブルじゃなくなっているし、一部の環境ではBuggyらしいし、毎回、こんな風にライブラリの入れ替えがあると嫌だなぁと思った。というか、HaskellはF#のように何でもアリな言語(こういうカオスな言語の方が結局は生き残る)ではないし、拡張しようとすると末期Perlのような無理が出てくると想像するのだが。Haskellはパーサを何度も書く時に強さを発揮すると思うので(それ以外の強みは簡単なパズルを解く時とか、並列処理とかが強化されても、そんな複雑な計算するのは一部の研究者でしかないし、アルゴリズムが確定すればCで書きたくなるし(Cで書くだけで最悪でも2倍程度は速くなるから))、文字列処理系のライブラリを使いこなせるようにしたいと思った。