[プログラミング] ウィンドウ移動中のメッセージループは別物
MFC や Windows フォームを使わずに、ウィンドウを使うアプリを作るときは、メッセージループが出てきます。でも、このメッセージループで何か処理をしよう、というのは Windows から見たら邪道のようです。
Windows プログラムをスクラッチから書いていたときの話です。
メッセージループが動いている様子を確認するために、PeekMessage() API を使って以下のコードを書きました。
MSG msg;
for (;;)
{
BOOL msg_arrived = ::PeekMessage( &msg, NULL, NULL, NULL, PM_REMOVE );
if ( msg_arrived )
{
if ( msg.message == WM_QUIT )
break;
::TranslateMessage( &msg );
::DispatchMessage( &msg );
}
// メッセージループが動いていることを確認する何かの処理
SomeOperationInEveryMessageLoop();
}
RegisterClassEx, CreateWindow を呼び出してウィンドウを作った後、メッセージループに入り、SomeOperationInEveryMessageLoop() 関数がちゃんと呼ばれていました。
ところが、そのウィンドウのタイトルバーの上でマウスの左ボタンを押したままにすると…、メッセージループ中の SomeOperationInEveryMessageLoop() が呼ばれなくなりました。おや?
「titlebar drag message loop」でググってみると、MSDN のフォーラムがヒットしました。
それによれば、マウスクリックによって Windows 内部のモーダルループに制御が移ってしまうとのこと。なんと。ちなみに下記はその書き込みの引用。
Clicking on the title bar starts a modal message loop inside of windows to allow the user to move the window.
自作のメッセージループが回らない今回の謎は解けましたが…、これで終わるのもなかなかもったいないので、解決法はないものかと、同じキーワードで続けてググってみて、もっともそれらしい解決法を発見。
メッセージループそのものには割り込めないから、タイマーでメッセージを送らせて処理するしかない、ってことです。
DialogBox() や MessageBox() を呼び出したときには内部メッセージループに入ることは知っていましたが、まさかウィンドウの移動やサイズ変更でも内部のループに入ることは知りませんでした。おそらく、別のループに入る条件はほかにもたくさんあるでしょう(あったような気がしますが、思い出せません…)。それを踏まえると、定期的な処理をしたいがためにアプリのメッセージループを使うのは、あまりよい方法ではなく、そういう処理をしたいのであれば、素直にタイマーを使いなさい、ということでしょう。
[プログラミング] Rake を使ってみる
Rake という新しいオモチャに手を出している今日この頃.Rake というのは make っぽい機能を提供してくれる Ruby のモジュールです.
make と比べると,make が文字列しか扱えないのに対して,Rake は Ruby の上で動くので,もう文字列,配列,ハッシュなどを慣れ親しんだ Ruby の機能でいろいろ操作できます.便利.
もう 1 個の利点といえば... cp や rm といったコマンドの名前が環境ごとに異なるということを考えなくともよいということでしょうか.書き手は FileUtils の関数を使って Rakefile (make でいう Makefile です) を書いておけば,あとは Ruby が吸収してくれるので楽です (コンパイラやリンカはコマンドが存在しないとダメですが...).
さて,Rake の使い方はいでさく氏のサイトに詳しく書いてあるのでそちらを参照するとして (私もそこで学びました),一通りの使い方が分かったところでさぁ書いてみようと,まずは以下の Rakefile を作りました.
TARGET = "test"
# Rake は最初に default と名づけられたタスクを実行する
task :default => TARGET
SRCS = FileList[ "test.cc", "test2.c" ]
OBJS = SRCS.ext('o')
# OBJS をリンクして実行ファイルを作る
file TARGET => OBJS do |t|
sh "g++ -o #{t.name} #{t.prerequisites.join(' ')}"
end
# .o のファイルは .cc や .c から作られるということを教える.
# ファイル名を明示せずとも,Rake が自動的にこのルールを適宜当てはめてくれる.
rule ".o" => ".cc" do |t|
sh "g++ -c -o #{t.name} #{t.source}"
end
rule ".o" => ".c" do |t|
sh "gcc -c -o #{t.name} #{t.source}"
end
これを test.cc と test2.c のあるディレクトリで実行すると,次のようにちゃんとビルドが実行できます.
$ rake (in /home/hoge) g++ -c -o test.o test.cc gcc -c -o test2.o test2.c g++ -o test.exe test.o test2.o $
うまく動きました.test.cc だけ touch してから再実行すると,make と同様にちゃんと test.cc だけ再コンパイルしてくれます.
ここで,ソースコードは全て ./src に,オブジェクトファイルは全て ./objs に分けたい場合は,rule ".o" => ".cc" が使えない(マッチしない)ので,Rakefile を次のように書く必要があるみたいです.
TARGET = "test"
task :default => TARGET
OBJ_DIR = "objs"
SRCS = FileList[ "src/test.cc", "src/test2.c" ]
directory OBJ_DIR
file TARGET => OBJS do |t|
sh "g++ -o #{t.name} #{t.prerequisites.join(' ')}"
end
# SRCS の各要素に対して 1 個ずつファイルタスクを作る
SRCS.each do |src|
obj = src.sub( %r{^src/(.*)\.(cc|c)}, OBJ_DIR+'/\1.o' )
file obj => [OBJ_DIR, src] do
sh "g++ -c -o #{obj} #{src}"
end
end
コンパイル前にオブジェクトファイル用のディレクトリを作ってもらわないといけないので,directory タスクを付け加えています.
最初は下のように書いてみたりもしましたが,Rake を実行時に objs/test.o の作り方が分からんと言われてしまいました.
TARGET = "test"
task :default => TARGET
# ソースは src ディレクトリ
SRCS = FileList[ "src/test.cc", "src/test2.c" ]
# 中間ファイルは objs ディレクトリ
OBJS = SRCS.sub(/^src\//, 'objs/').ext('o')
file TARGET => OBJS do |t|
sh "g++ -o #{t.name} #{t.prerequisites.join(' ')}"
end
rule ".o" => ".cc" do |t|
sh "g++ -c -o #{t.name} #{t.source}"
end
rule ".o" => ".c" do |t|
sh "g++ -c -o #{t.name} #{t.source}"
end
それにしても,Rake 面白いですね.
[プログラミング] 3x3逆行列の計算は直接やった方が速い
まあ,わざわざ書くまでもないことかも知れないのですが.
3x3くらいの逆行列だったらライブラリ使うより自分で計算した方が速かった,という話です.
対戦相手は CLAPACK + CPPLapack の組み合わせ.
使った CLAPACK は Version 3.0 で,ATLAS は使っていません.CPPLapack は 2007/10/28 に svn checkout したものです (2005_03_25 版だと下のコンパイルエラーが出るので,当時の最新版をチェックアウトして使いました.そしてごめんなさい.チェックアウトしたリビジョンを忘れてしまいました).
dgbmatrix-misc.hpp(176) : error C2061: 構文エラー : 識別子 'A' dgbmatrix-misc.hpp(178) : error C2440: '=' : 'double **(__cdecl *)(void)' から 'double **' に変換できません。この変換が可能なコンテキストはありません。
話が脱線しますが,2005_03_25版で,上のエラーが出ている行を見ると,下のように書いてあります.
double** A_darray(A.Darray);
これは double** の値を初期化するつもりで書いていますが,コンパイラは関数宣言に見えるものは全て関数宣言として扱うので,これを関数宣言として扱おうとしてコンパイルエラー,というわけです.
最新版では次のように書いてありますので問題ありません.
double** A_darray = A.Darray;
上の CPPLapack のエラーに関する話は,Belution.com VC++ フォーラムのどこかに書いてあったものです.ですが,残念なことに今 Belution.com のサーバがダウンしているようで全くフォーラムを見られません.
閑話休題.まずは CLAPACK+CPPLapack を用いた場合の逆行列計算関数(Inverse1)のソースを載せます.
CPPL::dgematrix Inverse1( const CPPL::dgematrix& mat )
{
return CPPL::i(mat);
}
まあ,これは CPPLapack の関数を呼んでいるだけです (しかし... 逆行列の関数名が i とは...).
続いて,手計算バージョン(Inverse2)のソース.
CPPL::dgematrix Inverse2( const CPPL::dgematrix& mat )
{
CPPL::dgematrix ret(3, 3);
double a = mat(0, 0);
double b = mat(0, 1);
double c = mat(0, 2);
double d = mat(1, 0);
double e = mat(1, 1);
double f = mat(1, 2);
double g = mat(2, 0);
double h = mat(2, 1);
double i = mat(2, 2);
double det = (a*e*i + b*f*g + c*d*h) - (a*f*h + b*d*i + c*e*g);
ret(0, 0) = (e*i-f*h)/det;
ret(0, 1) = (h*c-i*b)/det;
ret(0, 2) = (b*f-c*e)/det;
ret(1, 0) = (f*g-d*i)/det;
ret(1, 1) = (i*a-g*c)/det;
ret(1, 2) = (c*d-a*f)/det;
ret(2, 0) = (d*h-e*g)/det;
ret(2, 1) = (g*b-h*a)/det;
ret(2, 2) = (a*e-b*d)/det;
return ret;
}
それぞれを 1000000 回ずつ呼び出して,実行時間を計測しました.WinXP(SP2)+VC8(Express)です.また,時間計測には高分解能パフォーマンスカウンタ(QueryPerformanceCounter() API)を使いました.
その時間計測結果がこちら.
関数名 | 時間[秒] |
---|---|
Inverse1 | 1.403501 |
Inverse2 | 0.400147 |
ええ,3x3くらいなら自前で計算した方が速い,というわけです.
そして最後にオチ.「逆行列を100万回計算してもたった 1.4 秒しか掛かってないので,こんなところを速くしても全体は速くならないっ!」(アムダールの法則)
以上.おあとがよろしいようで.
[プログラミング] PGM ファイルの書き出し in Windows
Windows 上でプログラムを書いていて,24-bit 画像のピクセルデータを std::vector<BYTE> 上に保持し,それを PGM (P5; バイナリ形式) 形式で手っ取り早くファイルに書き出そうと思い,以下のコード片を書きました.
std::vector<BYTE> pixel_data;
...
std::ofstream out("temp.pgm");
out << "P5\n" << width << " " << height << "\n255\n"; // width と height はそれぞれ画像の幅と高さ
for (std::vector<BYTE>::iterator it = pixel_data.begin(); it != pixel_data.end(); ++it)
{
char p = *it;
out.write(&p, 1);
}
これが見事に大はまり.出力した PGM ファイルを IrfanView で見ると... 1 ピクセル横にずれるのです.原因がさっぱり分からず数時間たっぷり悩んでいました.
悩みながら,PGM (P5) の代わりに PPM (P6) で書き込むと,RGB の成分ずれが起こったので,今回の原因に気づきました.テキストモードで書き込んでいたため LF が CR+LF に変換されていたというわけです.
以下のように,バイナリモードでファイルを開いて書き込めば万事解決.
std::vector<BYTE> pixel_data;
...
std::ofstream out("temp.pgm", std::ios::binary);
out << "P5\n" << width << " " << height << "\n255\n"; // width と height はそれぞれ画像の幅と高さ
for (std::vector<BYTE>::iterator it = pixel_data.begin(); it != pixel_data.end(); ++it)
{
char p = *it;
out.write(&p, 1);
}
今まで散々 LF オンリーな環境でプログラムを書いていたので,全然気づきませんでした.あー,なんて凡ミスをしてるんだろ俺.
[プログラミング] ダイナミックリンクのススメ
Static Linking Considered Harmful という文書がありました.プログラムを作成する際に,スタティックリンクではなくダイナミックリンクを使いましょう,という内容の主張が書かれています.私もこれに賛成です.
原文にはスタティックリンクをした際の問題点がセキュリティに関するものなどを含めて 7 点挙げられています.この中で最も私の身近にある問題点が,そこに書かれている 1 番目の問題点でしょう.以下にその部分を引用します.
fixes (either security or only bug) have to be applied to only one place: the new DSO(s). If various applications are linked statically, all of them would have to be relinked.
強調箇所 (強調筆者) が身近に感じた点です.スタティックライブラリを書き換える度に実行プログラム本体を再リンクするのは面倒です.これだけでもダイナミックリンクを使う十分な理由になるでしょう.
もちろん今頃になってこんなことを考えたわけではないのですが,こういった文書を読む度に,自分が今作っているプログラムを libtoolize したくなります.ライブラリが多いので時間があるときにやってみることにします.
[プログラミング] Doxample
今回は Doxample というものを見つけたので,試しに使ってみたというレポートです.
さて,Doxample とは何かといいますと,ドキュメント生成ソフト Doxygen と,ビルド自動化ツール GNU Autoconf, GNU Automake, GNU Libtool (合わせて GNU Autotools と呼ばれる) の統合サンプルです.
どうやら,Autotools に Doxygen の設定ファイルを生成させてみよう,というもののようです.では試してみます.
Doxample にある doxample-0.1.tar.gz を展開すると,色々なファイルが出てきますので,とりあえず configure を起動してみます.
$ ./configure checking for gcc... gcc checking for C compiler default output file name... a.out (中略) checking for doxygen... /usr/bin/doxygen checking for perl... /usr/bin/perl checking for dot... /usr/bin/dot checking for pdflatex... /usr/bin/pdflatex checking for makeindex... /usr/bin/makeindex (中略) config.status: creating Makefile config.status: creating config.h config.status: executing depfiles commands $
/usr/bin/doxygen や /usr/bin/dot 等の存在をチェックしてくれています.コマンドがもし見つからなかった場合はどうなるか,試してみました.
$ sudo mv /usr/bin/doxygen /usr/bin/doxygen.bak Password: $ sudo mv /usr/bin/dot /usr/bin/dot.bak $ ./configure checking for gcc... gcc checking for C compiler default output file name... a.out (中略) checking for doxygen... no configure: WARNING: doxygen not found - will not generate any doxygen documentation (中略) config.status: creating Makefile config.status: creating config.h config.status: executing depfiles commands $
となりました.Doxygen がない環境でもエラーにはならないようです.
/usr/bin/dot がない環境も想定してみます./usr/bin/dot.bak はそのままにしておき,/usr/bin/doxygen だけを元の名前に戻して configure を実行してみました.
$ sudo mv /usr/bin/doxygen.bak /usr/bin/doxygen $ ./configure checking for gcc... gcc checking for C compiler default output file name... a.out (中略) checking for doxygen... /usr/bin/doxygen checking for perl... /usr/bin/perl checking for dot... no configure: WARNING: dot not found - will not generate graphics for doxygen documentation checking for pdflatex... /usr/bin/pdflatex checking for makeindex... /usr/bin/makeindex (中略) config.status: creating Makefile config.status: creating config.h config.status: executing depfiles commands $
となりました.しっかりした作りになっているようです.
dot も元に戻して,再 configure を行い,今度は make を実行します.
$ make distclean && ./configure (中略) (中略) $ make make all-am make[1]: Entering directory `/tmp/doxample-0.1' if gcc -DHAVE_CONFIG_H -I. -I. -I. -g -O2 -MT doxample.o -MD -MP -MF ".deps/doxample.Tpo" -c -o doxample.o doxample.c; \ then mv -f ".deps/doxample.Tpo" ".deps/doxample.Po"; else rm -f ".deps/doxample.Tpo"; exit 1; fi gcc -g -O2 -o doxample doxample.o rm -rf doxygen-doc SRCDIR='.' PROJECT='doxample' DOCDIR='doxygen-doc' VERSION='0.1' PERL_PATH='/usr/bin/perl' HAVE_DOT='YES' DOT_PATH='/usr/bin' GENERATE_MAN='YES' GENERATE_RTF='NO' GENERATE_XML='NO' GENERATE_HTMLHELP='NO' GENERATE_CHI='NO' GENERATE_HTML='YES' GENERATE_LATEX='YES' /usr/bin/doxygen ./doxygen.cfg
make 一発で doxygen が起動しました.doxyfile (doxygen.cfg) を書き換えるのかと思っていたら,環境変数を経由してパラメータを渡していました.
make clean を実行すると,生成したドキュメントも消えるようになっています.
Doxample オリジナルの make ターゲットは以下の通りです.
- doxygen-doc : 全てのドキュメントを生成する.
- doxygen-run : Doxygen を実行するだけ.man, ps, pdf 形式のドキュメントに関する後処理は行わない.
- doxygen-man : 生成した man pages の名前を変更する.
- doxygen-ps : PostScript 形式のドキュメントを生成する.
- doxygen-pdf : PDF 形式のドキュメントを生成する.
ではこれをどうやって自作のパッケージに組み込むのかを調べてみると... 簡単に組み込めそうだということがわかりました.
まず,Doxample オリジナルの M4 マクロ定義が入っている acinclude.m4 内の記述を,現在使用している acinclude.m4 に追加.このとき既存の acinclude.m4 が無ければ新たに作成します.
次に,aminclude.am をプロジェクトのトップディレクトリにコピーして,Makefile.am に以下の記述を追加します.
include aminclude.am MOSTLYCLEANFILES = $(DX_CLEANFILES) EXTRA_DIST = $(DX_CONFIG)
もし man page 形式のドキュメントを生成するのであれば,Doxample の例に倣って以下の記述も Makefile.am に施しておいた方が良さそうです.ただし,ドキュメントファイル名 doxample.c.1 の名前は適宜書き換える必要があるでしょう.
if DX_COND_man # You'd probably want to post-process man pages and installed the patched # versions. dist_man1_MANS = @DX_DOCDIR@/man/man1/doxample.c.1 $(dist_man1_MANS): doxygen-doc endif
さらに configure.in に以下の記述を追加します.このとき,Doxample の例に倣って,AM_INIT_AUTOMAKE より前に入れておきます.
DX_HTML_FEATURE(ON) DX_CHM_FEATURE(OFF) DX_CHI_FEATURE(OFF) DX_MAN_FEATURE(OFF) DX_RTF_FEATURE(OFF) DX_XML_FEATURE(OFF) DX_PDF_FEATURE(OFF) DX_PS_FEATURE(OFF) DX_INIT_DOXYGEN(Project name, doxygen.cfg)
上記 Project Name の部分は適宜置き換えてください.作成するドキュメントの種類や doxygen.cfg の部分は適宜変更しても問題ないでしょう.
ファイルを書き換え終えたら,aclocal.m4, configure を再作成します.
$ aclocal $ autoconf
これで完了です.
試しに自作パッケージで ./configure && make を実行してみると,今度は make 一発で自動的に Doxygen が実行されませんでした.何か記述が足りない可能性がありますが,make で毎回 Doxygen を動かすよりは,手動で make doxygen-doc を実行する方が都合が良さそうなので,今回はここまでとしておきます.
[プログラミング] configure for Qt Application
Qt や KDE アプリケーションは moc ファイルを使用するため,automake が出力した Makefile.in をそのまま使用してもコンパイルが成功しません.
The Makefile.am Howto に示されている手順に従って Makefile.am を書いたのち,kdesdk パッケージ内の am_edit という Perl スクリプト(私の環境では /usr/kde/3.4/share/apps/kapptemplate/admin/am_edit にありました)を実行する必要があります.例えば,src/Makefile.in を変更するのであれば,次のように入力します.
% /path/to/am_edit --path=/path/to/am_edit src/Makefile.in
[プログラミング] aclocal と Autoconf Macro Archive
autoconf を実行するために必要な aclocal.m4 を作成するかですが,aclocal を用います.
aclocal は configure.in で参照されているマクロの定義部分を抜き出して aclocal.m4 を生成するためのツールです.
デフォルトのマクロ定義は autoconf に付属しています(/usr/share/aclocal 等にある .m4 ファイルです).しかし,そうでないマクロは,それぞれのパッケージに付属しているか,あるいは Autoconf Macro Archive(Gentoo では sys-devel/autoconf-archive)パッケージに入っています.
このようなパッケージをインストールすると,マクロ定義の入った .m4 ファイルが大量にインストールされます.あとは,この定義を aclocal.m4 に取り込めば,autoconf を実行して configure を生成することができるようになります.
[プログラミング] autoconf の仕組み
GNU autoconf は,マクロプロセッサ GNU m4 を用いて,configure.in ファイルのマクロを展開してシェルスクリプトを作成するようです.
例えば,configure.in に書いたマクロ "AC_PROG_CC" は,autoconf を実行することで "checking for gcc..." を実行する一連のシェルスクリプトに展開されます.
そのマクロ定義本体は configure.in と共にある aclocal.m4 や acinclude.m4 に入っているわけです.
[プログラミング] GNU make の単純展開変数
TEXINPUTS を自動的に設定してタイプセットを行う Makefile を書こうとして,次のように書くと失敗します.
% cat Makefile TEXINPUTS=$(TEXINPUTS)./eps: all: foo.tex platex foo.tex % make platex foo.tex make: *** Recursive variable `TEXINPUTS' references itself (eventually). Stop.
この原因は,make は変数(マクロ)の展開を,それが実際に使用された時点で行うためです.
変数に値を付け加えたい場合は,"=" の代わりに ":=" を使用して単純展開変数
を定義します.
% cat Makefile TEXINPUTS := $(TEXINPUTS)./eps: all: foo.tex platex foo.tex % make platex foo.tex (snip) %
この場合,変数が定義された時点で 1 度だけ展開されます.ただし GNU make でないと使えません.
[プログラミング] SharpDevelop
SharpDevelop を試してみました.概観は Visual Studio .NET の IDE とそっくりです.
起動時にアセンブリからクラス情報データベースを 1 度だけ構築する必要がありますが,ちゃんと IntelliSense も使えます.構文強調規則も細かく制御できるようになっています(細かすぎて変えるのが面倒なくらいに).フォーム デザイナも装備していました.
SharpDevelop 本体の日本語リソースは SharpDevelop-jp が提供しており,適用することで殆どのメッセージが日本語になります.
Visual Studio .NET でいうソリューションとプロジェクトは,SharpDevelop のものと互換性がありません.しかし,SharpDevelop が Visual Studio .NET 形式のソリューションとプロジェクトをインポートできます.エクスポートはまだ調べてません.できなかったらどうしましょう.
また,SharpDevelop はデバッガを装備していないので,デバッグには .NET Framework SDK 付属の Microsoft CLR Debugger を使用する必要があります.
こんなところでしょうか.ちらほらと文字化けが目に付きますが,私は現在 SharpDevelop + CLR Debugger で十分なのでは? と甘い期待を抱いています.
[プログラミング] WTL
掲示板で見かける "WTL" という単語.私は ATL の typo と思っていたのですが... Windows Template Library の略だったのですね.
WTL は,MFC よりシンプルな Win32 API の wrapper のようです.
wrap している範囲は MFC より狭いようで,そこは発展途上という感じでしょうか.
C# と .NET Framework に移行しつつある今... 発展するのかな.