makeを使ったライブラリのビルド管理と暗黙ルールデータベースの話
CでもC++でも良いんだけど、コードが大きくなってくるとファイルの分割を考え始める.
追記(204/1/30):このままコンパイラをg++にするとコンパイルが通らなかった.また、-Wallオプションを付けないというとんでもないことをしている.その辺のことをこちらの記事に書いたので併せて参照をば.
例えば以下のようなことをしたいと考えたとする.
- ライブラリとなるソースコード(hello.c)をコンパイルして、arコマンドで静的ライブラリ(hello.a)を作る.
- ライブラリを呼び出すソースコード(run.c)のコンパイル時にライブラリをリンクして実行ファイルを生成する.
これらを手でやろうと思うとこれだけでもう大変なのでmakeを使う.
これでおしまいにしようかと思ったけど、なんでこれで上手く行くのか毎回忘れるので簡単にまとめることにした.
今回はこのサンプルを使って、暗黙ルールデータベースを見てみる.
追記(2014/1/24):libhello.a: libhello.a(hello.o)
というのは決して一般的な記法ではなく、アーカイブファイル用の記法なので注意(こちらを参照).
gistコマンドでターミナルからGistにコードを投稿する
Gistにコードを投稿するのにいちいちブラウザを介するのは煩わしいので、ターミナルからコマンド一発で投稿したい.
と思ったらあった.
https://github.com/defunkt/gist
インストール
$ sudo apt-get install ruby $ sudo apt-get install gem $ sudo gem install gist
手元のOSはDebian系ディストロです.
ヘルプ
$ gist -h $ gist --help
ログイン
$ gist --login
一回ログイン完了させておけばOK.後は以下のコマンドを叩くだけでサクッと投稿できるようになる.
投稿
$ gist hoge.c $ gist -d "description" hoge.c
説明文付きで投稿するなら-dオプションを使う.
使ってみた
$ gist -d "makeでライブラリのビルドを管理する(サンプル)" Makefile hello.* $ gist run.c -u https://gist.github.com/8523429
このように複数のファイルを指定可能.もちろんワイルドカードも使える.
既存のgistにコードを追加したければ-uオプションを使う.
既存のGistにあるコードを修正
Makefileが間違っていたので、これを修正した.
$ gist Makefile -u https://gist.github.com/8523429
単に修正したソースファイルをもう一度投稿するだけで上書き修正できる.ちゃんとリビジョンも更新されてdiffも確認できた.便利.
他にも標準入力やクリップボードからコードを投稿できたりして、物凄くカジュアルにコマンドを叩ける.
便利というかめちゃくちゃ簡単ですね.やっぱり何でもCLIでやれると気持ち良い.
二十五日半狂乱、7日目(の分)の記事
Cのエラーハンドリングと例外設計、例外処理のメモ
二十五日半狂乱、6日目(の分...orz)の記事
Cのエラーハンドリングを毎回やるのは面倒だ!
前回も言ったが、Cではエラーハンドリングに戻り値とerrnoを用いる.
それはそうと例外設計において"無視"は大罪である.
だから、関数を呼び出したら戻り値は漏らさずチェックすべきだ.
ということで、例えば以下のように逐一戻り値をチェックする.
if(send(sockfd, buf, len, 0) < 0){ ERROR("send"); exit(1); }
あぁ、面倒だ.
一体コードのどの部分が正常系の処理なのか?
ほとんどエラーハンドリング*1で埋め尽くされるじゃないか.
そもそもエラーハンドリング部分に書くのは毎回同じコードだし、コードの繰り返しは防ぎたい.
エラー処理部分をラッピングして楽をする
unpv12eの中でラッパーを被せることによってこの面倒を回避する方法を知った.
int Socket(int family, int type, int protocol) { int n; if ( (n = socket(family, type, protocol)) < 0) err_sys("socket error"); return(n); }
同書では、一貫して上記のように大文字で始まる関数名をラッパー関数として、小文字で始まる同じ名前の関数を呼び出す.
さらに前回記事のエラーメッセージを吐くようにすれば、戻り値のチェックもしなくて良いし、どこでエラーが起こったかもログから一目瞭然になると考えたので、上記のコードを(socket(2)じゃなくてsend(2)だけど)少し拡張した.
ヘッダに上記のマクロとラッパー関数のプロトタイプ宣言を仕込んでおけば、sendが必要な箇所では単にSend(sockfd, buf, len, 0);
と書くだけで良い.
これは楽である!しかも読みやすい!fopenやfgetsなどの関数もラッピング、なんならprintfもラッピングしても良い.
unpv12e(p12)にも、
ラッパー関数の仕様には、closeやlistenなど、普段エラー状態が無視されてしまう関数のエラーを検査できるという、望ましい副作用がある.
とあるではないか.素晴らしい*2.
おかげ様でコードは正常系の処理のみを記述したすっきりしたものになった.めでたし、めでたし.
※ただし、エラー発生時に即座にプロセスを終了する場合に限る
正常系、準正常系、異常系
unpv12e(p12)にはさらに以下の様な記述がある.
本書の残りの部分では、明示的なエラーの検査とプロセスを終了させる以外のエラー処理が必要な場合を除いて、これらのラッパー関数を使用する.
単にエラーが起これば終了すれば良いコードを書いているうちは気楽だったが、プロセスを終了させる以外のエラー処理が必要な場合のような状況にぶち当たった瞬間、エラー処理がとんでもなく悩ましいものになってしまった.
上に挙げたsend(2)は、成功した場合、送信されたバイト数を返すが、エラーの場合、-1を返し、errnoを適切に設定する.
プログラマはエラーの原因に応じてプロセスを終了するか、適切なエラー処理を行った上で処理を復帰するか判断しなければならない.
極端なことを言えば、通信途中にコネクションが切れた場合は異常系か?準正常系か?タイムアウトが起こったらそれは異常系か?準正常系か?などをいちいち判断しなければならない.
死ぬべきものを生かしてはならないし、準正常系ならば呼び出し元で対処せよというのは、呼び出し元で中途半端になった処理を巻き戻す処理が必要になったことがある.
そして何が準正常系なのかはアプリケーションの要件次第である.
例えば、サーバプログラムにクライアントからのアクセスが集中しすぎて、fopen(3)やaccept(2)でファイルディスクリプタ不足に陥ったことがある.
そのような状況に陥って初めてそりゃそうかと思ったが、実際にそんな問題が起きるまでは自分の中では特に問題のないコードだった.
こんなしょーもない例だと想像力が足りないと言われてしまいそうだけど、もっと複雑な事象だと予測も難しそうだし、この辺は壮絶に経験が生きそう場面であるような気がする.
手元のアプリケーションでは、コネクションがタイムアウトしてしまう前にファイルを開きたかったので、以下のようにディスクリプタ不足でファイルが開けない場合は、しばらく待って再度ファイルオープンを試みるようにした.
FILE * fopen_safefd(const char *filename, const char *mode) { FILE *fp = NULL; while(1) { fp = fopen(filename, mode); if(fp != NULL) { break; //ファイルオープン成功 } else if(errno == EMFILE) { //ディスクリプタ不足 WARNNING("file=%s", filename); sleep(1+rand()/((double)RAND_MAX+1.0)); //ディスクリプタが空くのを待つ1+(0~1)秒 errno = 0; } else { ERROR("fopen failed:file=%s", filename); exit(1); } } return(fp); }
なんかこうして見ると放置していた問題が山ほどあるような….
・準正常系は呼び出し元で対処せよという規律を無視している.
・待ち時間の1+(0~1)秒という時間に明確な根拠がない.
・ファイルが開けるまでいつまでもオープンを繰り返す問題を持っている.
参考:エラー処理の抽象化#46
まぁ、なんせ設計の段階で準正常系を考慮に入れておかないとAPI設計が妙なことになることがなんとなくわかってきた.
例外設計というのはどうやら想像以上に難しいらしい.
例外処理の話とか、LOG.debug("nice catch!")とか
力尽きたので後は書こうと思っていたことのメモ書き.
発生したエラーが準正常系だった場合、呼び出し元にエラーを伝播させる必要があるが、関数呼び出しがネストした場合にその労力が半端じゃない.
→ もしかして例外処理はその辺から生まれた?
例外は、深い階層で発生したエラーを上位層でハンドリングするために、エラー値を伝搬していくのが手間であるという動機の元に導入された。 エラーハンドリングに関する考え
→ そういえば、「コーディングを支える技術」では、"失敗したらジャンプする"という発想がC言語よりもずっと前の言語からあったという話を起点に、例外処理の歴史をUNIVAC I, COBOL, PL/I,CLU,C++,Windows NT 3.1, D言語, Python, Ruby, JavaScript, Java, C#といった言語の設計思想等も交えて議論されていた.
すごく良いように見えるJavaの検査例外が何故流行らないのかという話が面白い.曰く、「一言で言ってしまえば、面倒くさいからでしょう.」
ちなみにLinuxカーネルでは例外処理(リソース処理)にあたる処理をgoto文で行っているらしい.
コーディングを支える技術 ~成り立ちから学ぶプログラミング作法 (WEB+DB PRESS plus)
- 作者: 西尾泰和
- 出版社/メーカー: 技術評論社
- 発売日: 2013/04/24
- メディア: 単行本(ソフトカバー)
- この商品を含むブログ (27件) を見る
→ 例外処理がさまざまな言語で採用される一方でGoogleのコーディング規約ではC++で例外使うなとかいう話もある.
→ 確かに例外処理が最強であると世の中的に満場一致してないっぽい例としてGo言語のタプルによるエラーハンドリング機構.Goのタプルは言語組み込みかつエラーを無視しにくいらしい.
→ ここまでいくつかスライドのリンクを張ってきたけど、エラーハンドリングと例外設計について凄く参考になった例外とロギング勉強会というものが2012/6/27に行われていたのでメモ.だいぶ有名っぽいけど.
参考:
java-ja『LOG.debug("nice catch!")』に参加してきた - Shinya’s Daily Report (最下部に他のブログやスライドのリンクもあります)
java-ja の例外とロギング勉強会で発表してきました - 純粋関数空間
java-jaで例外処理の話をしてきました - 西尾泰和のはてなダイアリー
※例外設計や例外処理の是非については結構意見が分かれるところみたいなので、ちゃんと知りたい場合はリンク先のブログ等も見てみると良いと思います.
その他参考:
マサカリ投げて下さい.
というわけで、風呂敷広げすぎて後半はもう何が言いたいのかも良くわからなくなったので、タイトルにメモと書いて逃走.
ブログ難しい.
間違ったこと言ってたらマサカリ投げて下さい.
*1:ここで例外処理というと誤解が生じるために、エラーハンドリングとかエラー処理などと表現しているのですが、一方で例外処理という言葉が一般に的な意味で解説されていたりして、本当に日本語って難しいです.
*2:ぶっちゃけ現在もこれで良しとされているのでしょうか?「今時こんな古い書籍のコーディングスタイルを妄信してるなんておめでたい奴だな.」という感じなんでしょうか?などという心配事は尽きないのですが、それはさておき
Cのエラーメッセージ出力に関数名や行番号を付加する
二十五日半狂乱、5日目(の分)の記事
C言語における関数のエラーハンドリングには戻り値およびerrnoを使うが、自分が読んだ文法書などのサンプルコードではエラーが起こった場合の処理が、大体がperror("fopen"); exit(1);
のような感じのもので、まぁ小さいサンプルコードをいくつも書いていたうちはこれでも良かった.
だけど、ある程度以上の規模のアプリケーションを書き始めて、コードがそれなりに大きくなってくると一体どこのエラーなのか一目でわからなくなってくる.
エラーメッセージを自由に設定して、関数名や行番号も表示したい.それに何度も書くのでできるならお手軽に書きたい.
そんな事情で、一年前くらい前に以下のようなエラーメッセージ用の関数を作った.コンパイラはgccを使用する.
続きを読む