yaml って Makefile を書くのに向いてるんじゃないか?
設定ファイルとして、私は yaml がお気に入りです。
なんといっても記述量が少ない。DSL もいいですが、やっぱり特化型には敵わないと思います。また、ライブラリが豊富で主要なプログラミング言語のほとんどで使えるというのも魅力です。
話は変わって「C/C++ 開発のストレスを軽減する」で触れたとおり、ビルドツールとして SCons、CMake がお気に入りです。ただし、SCons は Python なのでどうしても文字列を何らかの引用符で括る必要があるということに不満があります。CMake は専用の書式で書かなければならないということに不満があります。
例えば、SCons と CMake で hello world をビルドするときを考えてみます。
# Scons Program(target = "hello", source = ["helloworld.c"])
# CMake project(hello c) add_executable(hello helloworld.c)
これくらいだとどっちも十分にシンプルなので、どっちで書いてもいいかな、という気がしますね。仮に、このプログラムが順調に育ちソースコードが増えていったとしましょう。
# Scons Program(target = "hello", source = ["helloworld.c", "helloearth.c", "hellouniverse.c"])
# CMake project(hello c) add_executable(hello helloworld.c helloearth.c hellouniverse.c)
まだ大丈夫ですね。次に、プラットフォーム毎に異なるソースコードをコンパイルしたくなったとしましょう。
# Scons import os env = Environment() platform = env['PLATFORM'] sources = ["helloworld.c", "helloearth.c", "hellouniverse.c"] if platform == 'posix': sources.append('helloposx.c') elif platform == 'darwin': sources.append('hellodarwin.c') elif platform == 'win32': sources.append('hellowin32.c') env.Program(target = "hello", source = sources)
# CMake project(hello c) set(sources) if (UNIX) set(sources helloworld.c helloearth.c hellouniverse.c helloposix.c) elseif (APPLE) set(sources helloworld.c helloearth.c hellouniverse.c hellodarwin.c) elseif (WIN32) set(sources helloworld.c helloearth.c hellouniverse.c hellowin32.c) endif add_executable(hello ${sources})
どちらも一気に野暮ったくなりましたね*1。ここで、どういう風に書きたいかを考えてみましょう。
project: name: hello type: executable source: general: [helloworld.c, helloearth.c, hellouniverse.c] posix: [helloposix.c] darwin: [hellodarwin.c] win32: [hellowin32.c]
ソースコードがもっと増えていくことを考えると、次のように書くべきかもしれません。
project: name: hello type: executable source: general: - helloworld.c - helloearth.c - hellouniverse.c posix: - helloposix.c darwin: - hellodarwin.c win32: - hellowin32.c
これだとすっきりしますね。ソースコードひとつにつきハイフンがいるのが厄介ですが、引用符で括るよりも楽だと思います。
さらにプログラムが大きくなると、他のライブラリを使いたくなるかもしれません。そうすると、include ディレクトリの追加とか、lib ディレクトリを追加する必要が出てきます。
project: name: hello type: executable source: general: - helloworld.c - helloearth.c - hellouniverse.c posix: - helloposix.c darwin: - hellodarwin.c win32: - hellowin32.c incdir: - contrib/include libdir: - contrib/lib lib: - foo
条件付きコンパイルもやってみましょう。
project: name: hello type: executable source: general: - helloworld.c - helloearth.c - hellouniverse.c posix: - helloposix.c darwin: - hellodarwin.c win32: - hellowin32.c ENABLE_XXX: - xxx.c incdir: - contrib/include libdir: - contrib/lib lib: - foo
マルチプラットフォーム向けに開発するときには、リンクするライブラリやコンパイルするソースコードはプラットフォームごとに異なるので、もう少し書き方を工夫してみます。
project: name: hello type: executable source: - helloworld.c - helloearth.c - hellouniverse.c incdir: - contrib/include libdir: - contrib/lib lib: - foo platform_posix: source: - helloposix.c platform_darwin: source: - hellodarwin.c platform_win32: source: - hellowin32.c
platform_xxx では、トップレベルでの incdir、libdir、lib が使えるというイメージですね。
これだと、解析も簡単なのでこれから Autotools 用のファイルを作ったり、SCons 用のファイルを作ったり、Visual Studio 用のプロジェクトファイルを作ったり、nmake 用のメイクファイルを作ったりと、夢が広がります。既存のビルドツールからの移行を考えると、ここで挙げたような作りだけでは到底機能が足りませんが、まぁなんとかなるんじゃないかな、と思います。
ここで挙げた YAML は「Rubyist Magazine - プログラマーのための YAML 入門 (初級編)」の print-yaml.rb で出力を確認したので多分構文的に間違いはないと思います。
*1:もっといい書き方をご存知の方は是非ご教授を・・・
GUI アプリケーションのエントリポイントを並べてみる
Delphi
begin Application.Initialize; Application.CreateForm(TForm1, Form1); Application.Run; end.
Lazarus
begin {$I project1.lrs} Application.Initialize; Application.CreateForm(TForm1, Form1); Application.Run; end.
Qt
#include <qapplication.h> #include <qpushbutton.h> int main( int argc, char **argv ) { QApplication a( argc, argv ); QPushButton hello( "Hello world!", 0 ); hello.resize( 100, 30 ); a.setMainWidget( &hello ); hello.show(); return a.exec(); }
wxWidgets
#include "main.h" #include "simple.h" IMPLEMENT_APP(MyApp) bool MyApp::OnInit() { Simple *simple = new Simple(wxT("Simple")); simple->Show(true); return true; }
WinMain は中で持っているというタイプ。あまり好きじゃない。
Cocoa
#import <Cocoa/Cocoa.h> int main(int argc, char *argv[]) { return NSApplicationMain(argc, (const char **) argv); }
実際の処理は、classes.nib に書いてあるクラスから始まるっぽい。
Carbon
#include <Carbon/Carbon.h> int main(int argc, char* argv[]) { // ???? }
まだ調査中です。
GUI とマルチスレッド、GUI ツールキットの振る舞い
はじめに
プラットフォームによっては、GUI のコントロールはコントロールが所属しているスレッド以外から操作することは安全ではないことが多い*1。私が Windows でこのことを知ったが、どうやら大抵のプラットフォームで同じことが言える。これはおそらく、GUI を使ったシステムはそのためのメインループを何らかの形で持っており、そこに割り込むことが危険であるためであろう。今回は、それに関して有名な GUI ツールキットがどのように対処しているかを見ていく。
VCL ( Delphi )
Delphi は、Professional 以上のエディションを購入すると VCL のソースコードがついてくるという特典があった*2。手元に Delphi の環境がないのでソースコードを見ることができないが、現在は Free Delphi とでもいえる Lazarus というものがある。これに VCL に非常によく似たソースコードが入っているので、これについて見てみた。
VCL でスレッドを扱うときは、TThread というクラスを使う。これには Synchronize というメソッドがあり、これに関数を指定してやると、それが GUI スレッド上で実行される、という仕組みだ。しかし、このメソッドにはどの GUI スレッドで実行するかという指定がない。つまり、VCL のシステムに暗黙の何かがあるということだ。これはどうなっているのだろうか?
Lazarus をインストールすると、Delphi で利用可能な言語である Object Pascal のコンパイラ、Free Pascal もインストールされ、そこにソースがある。
- fpc/source/rtl/objpas/classes/classes.inc
TThread.Synchronizeは以下のような手順を踏んでいる。
- スレッドがメインスレッド ( GUI スレッド ) と同じスレッド ID かどうかをチェックする
- 同じならそのまま実行する
- SynchronizeCritSect クリティカルセクションに入る
- SynchronizeMethod に引数で指定されたメソッドポインタをコピーする
- DoSynchronizeMethod フラグをたてる
- SynchronizeTimeoutEvent をシグナルする
- WakeMainThread 関数にメソッドポインタを渡す
- ExecuteEvent がシグナルされるまで無限に待つ
- LocalSyncException に SynchronizeException をコピーする
- SynchronizeCritSect クリティカルセクションを終える
- LocalSyncException が何かあれば、例外を送出する
WakeMainThread というのは、以下にある。これはプラットフォーム毎に実装が異なり、Lazarus 側にソースがある。以下で、Windows の場合を取り上げる。
- lazarus/lcl/interfaces/win32/win32object.inc
- WM_NULL を AppHandle に向かって PostMessage する
で、以下に処理がうつる。
- lazarus/lcl/interfaces/win32/win32callback.inc
- CheckSynchronize する
- これは中身がfpc/source/rtl/objpas/classes/classes.incにある
- CheckPipeEventsする
- CheckSynchronize する
で、CheckSynchronize が呼び出されると、ようやく TThread.Synchronize で指定したメソッドがメインスレッドで実行される。CheckSynchronize の内部はこうなっている。
- スレッドIDがメインスレッドのものと同じかをチェックする
- 違ったら例外をはく
- SynchronizeTimeoutEvent を非シグナル状態にする
- DoSynchronizeMethod フラグが立っていれば、以下の処理を行う
- DoSynchronizeMethod フラグを false にする
- try catch で囲んで SynchronizeMethod する
- 例外を受け取ったらそれを SynchronizeException に保存しておく
- ExecuteEvent をシグナルして、TThread 側に完了を通知する
複雑だが、見れば見るほどよく出来ている。しかし、いい意味でも悪い意味でも VCL システムにべったりと依存しているので、この仕組だけを取り出すということは難しそうである。
.NET
Control.Invoke メソッドを使うと、スレッドのコンテキストスイッチを切り替えて GUI スレッド上でデリゲートを同期的に実行できる。また、Control.BeginInvoke メソッドを使うと同じようなことが非同期的に実行できる。内部の仕組みは Mono のソースコードなどを見れば分かるだろうが、見ていない。
Qt
Qt は最も普及しているクロスプラットフォームに対応した GUI ツールキットだと思う。Qt の場合、QThread::postEvent メソッドというものが用意されている。これはスレッドセーフであるらしい。
このメソッドは、対象となるウィジェットとイベントを指定するという、VCL の TThread.Synchronize に比べると随分素直な作りになっている。関数やらメソッドやらを指定して実行するタイプではない。このイベントというのは、QEvent というクラスで、Windows メッセージを抽象化したようなクラスである。イベントタイプにより何であるかを識別し、それ毎に任意の情報を与えることができるようになっている。
作りとしては安全だが、TThread.Synchronize、Control.Inboke に比べると利便性は落ちるような気がする。他にも何か方法が提供されているのだろうか?
wxWidgets
クロスプラットフォームに対応したフリーな GUI ツールキットのひとつ。wxWidgets では、wxwxMutexGuiEnter/wxMutexGuiLeaveを使い、これらで挟まれたスコープ内に処理を書く、という少し変わった実装になっている。
何か変だなと違和感を感じたので、Windows での実装のソースコードを追ってみたところ、GUI スレッドでコントロールを操作するのではなく、GUI スレッドを止めてから他のスレッドでコントロールする方式のようだ。これでうまく動くのだろうか・・・?内部では以下のように処理が進む。
- wxMutexGuiEnter
- wxWakeUpMainThread
- PostThreadMessage(WM_NULL)
- メインループがWM_NULLを受け取る
- theWxApp->ProcessPendingEvents
- ここまでくると処理が実行される
- メインループがwxMutexGuiLeaveOrEnterを実行する
- wxWakeUpMainThread
- wxMutexGuiLeave
この二つの関数に関しては、以下で少し触れられていて、これをやるよりも wxPostEvent を使った方がいいらしい。納得。
SWT ( Java )
Eclipse の登場で脚光を浴びた感のある SWT。以下のようなメソッドが提供されている。
- Display.syncExec
- Display.asyncExec
以下が参考になる。
エラーハンドリングに関する私なりのガイドライン
いつからか、通信に関するプログラミングを行う機会が多くなった。通信が成功することは奇跡なので、様々な状況で、様々な原因により、エラーが発生し、失敗する。プログラミングにおいてエラーハンドリングは重要だが、ネットワークプログラミングでも同様である。
というわけで、エラーハンドリングに関する私なりのガイドラインを書いてみる。
エラーが発生したときに、プログラムがとりうる行動がいくつかある。「続行」「再試行」「中止」の三つだ。自動*1・手動*2でこれらを選択するという観点を持って、詳しく考えてみる。
- 続行 ( Ignore )
- 再試行 ( Retry )
- 原因が致命的でない場合、選択できるべきである
- 再試行すれば成功する可能性があるため
- プログラムで自動的に何度かこれを選択し、駄目だった場合にエラーダイアログを出すなどすると利便性が向上する
- ただし、後述する共通事項で述べることを熟考しておく必要があるので注意すること
- 時間の経過によって状況が改善され再試行対象処理が成功する可能性がある場合、自動再試行の有効性は大きい
- ただし、自動再試行は永遠に行うのではなく、自動再試行回数を制限する、自動再試行時間を制限する、その間何らかの方法で利用者がこれを中止できるようにするなどしなければならない
- 原因が致命的でない場合、選択できるべきである
- 中止 ( Abort, Cancel )
- 原因に関わらず、選択できるべきである
- これができないと利便性を損ねるため
- 原因に関わらず、選択できるべきである
- 共通事項
- プログラムが自動的に何らかを選択する場合、それを仕様として明確にし、実施前に利用者が把握できるようにしなければならない
- でなければ、これによって利用者が大きな不利益を被るなど、トラブルの元になる
- そのような可能性がある場合、利用者に逐一確認を求めるのが無難である
- プログラムが自動的に何らかを選択する場合、それを仕様として明確にし、実施前に利用者が把握できるようにしなければならない
開発対象となるプログラムに応じて、これらを適切に決定する必要がある。開発対象が Linux でいうデーモン、Windows でいうサービスならば、基本的に途中で選択肢を提示する、ということはない。代わりに、それらを設定ファイルなどで与えることで振る舞いを調整する。開発対象が GUI や CUI をもつ対話型のアプリケーションならば、途中で選択肢を提示し、利用者に選択してもらうことによって振る舞いを調整する。よくできたアプリケーションならば、それらの振る舞いをある程度記憶し、次回からはそれに沿った振る舞いをすることができたりする。
また、共通事項として記述したが、プログラムが自動的に選択を行う場合は、特に注意が必要である。例えば、それらの選択によって利用者への課金が発生する場合などである。直接的に課金が発生しないにしても、通信環境によっては料金体系が定額制でなく従量制である場合、十分な注意が必要である。これは、携帯端末などの移動体通信環境下においてはよくあることである。
プログラミングという分類としては抽象的な内容になってしまったが、以上がエラーハンドリングに関する私なりのガイドラインである。ご意見、ご指摘をいただければ幸いである。
国際化ドメイン名 ( IDN ) について
国際化ドメイン名を使うと、ドメイン名にアルファベットや数字だけでなく日本語を含む様々な文字が利用出来るようになる。自分の書いたコードを、これに対応させるにはどうすればいいか?*1これは、ドメイン名から IP アドレスを得る話なので、名前解決の話である。名前解決といえば、まず思い浮かぶのは getaddrinfo() だろう。
Linux の場合、glibc 2.3.4 からは、AI_IDN というフラグを加えてやることで対応できる。Mac OS X や Windows ではそういったフラグは見当たらない。Windows では、IdnToAscii() によって得られた結果を getaddrinfo() に渡すことで国際化ドメイン名に対して名前解決ができるようになる。
では Mac OS X ではどうしているのだろうか、と思い調べてみると、何らかの外部ライブラリを利用している場合があるようだ。例えば、Mac OS X 専用の Twitter クライアントとして有な夜フクロウは、VeriSign が提供している IDN Library を使っているらしい。他にも、GNU が提供しているものや、JPNIC が提供しているものなど様々なライブラリがある。
- Software for International Domain Names - IDN Software Development Kit - Verisign
- GNU IDN Library - Libidn - GNU Project - Free Software Foundation
- 当然 GPL
- idnkit download
- おそらく BSD ライセンス
国際化ドメイン名は結構新しい仕組みなので、プラットフォームごとに対処がばらばらで中々に厄介である。Linux がとった対応方法が一番シンプルに見える。
では、上記ライブラリを使わずに国際化ドメイン名の名前解決をやるにはどうすればいいだろうか?それには、国際化ドメイン名とは何ぞやというのを知る必要がある。
- RFC 3490 - Internationalizing Domain Names in Applications (IDNA)
- RFC 3491 - Nameprep: A Stringprep Profile for Internationalized Domain Names (IDN)
- RFC 3492 - Punycode: A Bootstring encoding of Unicode for Internationalized Domain Names in Applications (IDNA)
大雑把にまとめるとこのような感じになる。
- 国際化ドメイン名に含まれる文字列をそのまま扱うのは既存資産の対応という意味で厳しい
- だったら国際化ドメイン名を既存資産が扱える文字列に変換してしまおう
- ベースとなる文字コードは UTF-32 でいいだろう
- 変換方式は Punycode と名付けよう
- 実際に国際化ドメイン名を変換するときは、ドメイン名をラベルごとに Punycode で変換し、「xn--」というプレフィクスをつけよう
ここで少し面倒なのが、Unicode のサポートである。が、Linux や Mac OS X ならデフォルトで UTF-8、Windows なら UTF-16 がサポートされている。そして、Unicode 同士の変換はそんなに複雑ではないのだ。*2
というわけで、実装するなら以下のような処理が必要になる。
- UTF-8,UTF-16 を UTF-32 に変換する処理
- LE 限定なら、C で書いても 200 行足らずで書ける
- Punycode 変換する処理
- RFC にのっているコードを参考にして実装できる
- C で書いても 200 行足らずで書ける
- ドメイン名をラベルで分割する処理
- ラベルを Punycode で変換する処理
- Punycode で変換したラベルで結合する処理
- 必要に応じて「xn--」というプレフィクスをつける処理
試しに書いたコードでは、C で一から書いて全部で 600 行くらいになった。C だとやっぱり長くなるな。JavaScript で書かれた実装なんかもあるので、興味のある方はどうぞ。
IDN はそんなに怖くない、ということで。お粗末。
MD5、MD4 のメモ
MD5 も Base64 の記事で書いたライブラリが大体実装している。MD4 は時代遅れなせいか、MD5 よりも実装が少ないようだ。MD4 が使われている例としては、Active Directory で活躍する Windows 統合認証などの基礎となる NTLM 認証などがある。
- MD5
- MD4
MD5 と MD4 は非常によく似ていて、RFC の内容もほとんど同じである。RFC で違うのは以下のようなところだ。
- 3.4
MD5 の機能を RSA のライセンスから逃れて使いたい場合には、RFC1321-based (RSA-free) MD5 libraryというものがある。ソースコードを見ると、いたるところでエンディアンを意識したコードが出てくるのだけど、RFC のコードはエンディアンに依存した処理があるということの表れなんだろうか。apr に載ってるのなら、その辺はクリアしていそうな気がするのだけれど・・・。
apr には、MD4、MD5、SHA-1 の三つだけが搭載されている。これ以上のことは mod_ssl にやらせるけど、普通にありがちな HTTP でこれらは利用されるということの表れなのかな。
Base64 って結構カオス?
Base64 のライブラリをあまり調べたことがなかったので調べてみた。
- RFC
- http://tools.ietf.org/html/rfc4648
- 標準で、一番あたらしいやつ
- http://tools.ietf.org/html/rfc3548
- 標準だが、obsoleted らしい
- http://tools.ietf.org/html/rfc2045
- MIME 用
- http://tools.ietf.org/html/rfc1421
- オリジナルらしいが、deprecated らしい
- http://tools.ietf.org/html/rfc4648
- Wikipedia
まず、Base64 には多くの変形版が存在するという点に注意する必要がある。自分で使いたいのはどういった目的なのか、それをライブラリはサポートしているのか、のようなことだ。変形版については、Wikipedia に詳しくのっているのでそれを参照されたし。変形版は以下のような違いがある。
- 62 番目の文字を何とみなすか
- 「+」プラス
- 「-」マイナス
- 「.」ピリオド
- 「_」アンダースコア
- 「!」エクスクラメーション
- 63 番目の文字を何とみなすか
- 「/」スラッシュ
- 「-」マイナス
- 「_」アンダースコア
- 「:」アンダースコア
- パディングの有無
- 「=」か「なし」
- 1行が固定長かどうか
- 行の最大長
- 64
- 76
- 何かに依存
- 改行文字
- CRLF ( 必要な場合のみ )
- なし
- 指定された文字以外を使えるかどうか
- 行のチェックサム
標準とされる Base64 は以下のとおり。ほとんどはこれをターゲットにしているはず。
- 62 番目の文字を何とみなすか
- 「+」プラス
- 63 番目の文字を何とみなすか
- 「/」スラッシュ
- パディングの有無
- 「=」
- 1行が固定長かどうか
- はい
- 行の最大長
- 64 or 76
- 改行文字
- CRLF ( 必要な場合のみ )
- 指定された文字以外を使えるかどうか
- 禁止
- 行のチェックサム
ここまで見ると、Base64 のライブラリはこれらをどこまでサポートしているのかという情報があって、こういった点を指定することができて然るべきだ、と思えてくる。では、実際にライブラリやコードを見てみよう。
- C/C++ ライブラリ
- OpenSSL: Documents, BIO_f_base64(3)
- Crypto++: base64.cpp Source File
- 改行するかどうかを指定できる、既定ではする
- 改行位置が指定できる、既定では 72・・・72?
- 改行文字は CRLF ではなく LF
- hamigaki
- 改行できない?
- 62、63 番目の文字は、+/ か -_ のペアを使うことができる
- b64: Base-64 Encoding Library - Synesis Software - C/C++ Software Libraries Resource Site
- RFC 1113 に対応しているらしい
- 62、63 番目の文字は、+/
- 改行文字は CRLF
- 改行位置は、64 か 76 推奨で任意に指定できる
- ドキュメントがかなり詳細に記述されている
ここに書いた情報はドキュメントやソースコードを見ながら書いたが、間違いがあるかもしれないので注意してほしい。詳しいドキュメントがあったのは、b64 だけだった。それにしても、どれもこれも狙ったかのように仕様がまちまちである。どれも、それほど RFC に気を取られていないように思える。特に OpenSSL がこれでは、他のコードは互換性維持のために RFC を無視してそれに合わせる必要が出てくるだろう。・・・中々厄介である。b64 は、改行文字が選択できればよいのだが・・・。
というわけで、ライブラリをそのまま使うだけでは何か問題に遭遇する可能性が高いので、外部インターフェースとの連携が必要な場合は特に注意が必要だろう。どなたか、C/C++ でいいライブラリ知りませんか?
気になったので他の言語も調べてみた。
- C/C++ 以外の言語編
- Perl
- Ruby
- base64 - Rubyリファレンスマニュアル
- RFC 2045 らしいので、b64 と似たような感じ
- 改行に関することはドキュメントからは分からない
- base64 - Rubyリファレンスマニュアル
- Python
- 12.12 base64 -- RFC 3548: Base16, Base32, Base64 テータの符号化
- RFC 3548
- 62、63 番目の文字を任意指定できる
- 改行位置は指定できないっぽい
- 12.12 base64 -- RFC 3548: Base16, Base32, Base64 テータの符号化
- Java
- Base64 (Commons Codec 1.4 API)
- RFC 2045
- 改行文字は CRLF、変更可能
- 改行位置は 76、変更可能
- 62、63 番目の文字は、+/ か -_ のペアを使うことができる
- Base64 (Commons Codec 1.4 API)
他の言語も結構まちまち。汎用性という意味では、Java のが一番よさげか。
きっと Bae64 は、その原理のシンプルさからみんながばらばらと実装していってこんな感じになったんだろうな。Base64 なら、この辺を細かく制御できるコードを書けば天下とれそうだ。細かいけど。
以下は調査中に見つけたコード。面白そうだったので挙げておく。