C言語で継続2

http://d.hatena.ne.jp/kilrey/20090921#p1で作成した継続は本当に「スタックを巻き戻す」ことしかできない。例えば、外部リソースを再利用できない、といった問題がある。

外部リソースを再利用できない。

下のソースコードでf1()で記録した継続をf2()で巻き戻した場合、ptrを二重free()してしまうというのが問題になる。もちろん、f1()でptrに読み書きするのも危険だろう。

void f0() {
  void* ptr = malloc(0x1000);
  f1(ptr);
  free(ptr);
  f2();
}

これは元々setjmp(), longjmp()が抱えていた問題の変種だ。例外とRAIIとにより元の問題は解決されているが、この問題は解決できていない。GCとRAIIとにより解決することはできるが、ゼロ・オーバーヘッド原則に反しており、わざわざC言語で行うべきものとは思えない。

void f0() {
  void* ptr = GC_malloc(0x1000);
  f1(ptr);
  f2();
}

サンドボックスで解決できないか。

サンドボックスの内部でのみxsetjmp(), xlongjmp()を利用することができる、というように条件を変えてみよう。本来free()を呼んでいた箇所ではサンドボックス終了時に呼び出したい関数とその引数を登録する。これならゼロ・オーバーヘッド原則にも反していないだろう。

void f0() {
  void* ptr = malloc(0x1000);
  f1(ptr);
  on_exit_from_sandbox(free, ptr);
  f2();
}

〆。

まあ、メモリの話に限ってしまえばApache Portable Runtimeのようなメモリプールを使う方が楽だろう。それ以外でも汎用に使えるのが利点ということで。

void f0() {
  FILE* fout= fopen(...);
  f1(fout);
  on_exit_from_sandbox(fclose, fout);
  f2();
}

C言語で継続

C言語で継続を実装できるかを考えてみた。もちろん、生のC言語でできるわけがないのだけど、環境を制限すればできないこともない。

前提、setjmp()とlongjmp()について。

setjmp()とlongjmp()の用途はあくまでも大域脱出である。そのため、setjmp()を呼び出した段よりも深いスタックフレームがすべて保存されていなければならない。

OK
stack setjmp
stack work1 ... workN longjmp
NG
stack work1 ... workN setjmp
stack longjmp

コピーしよう。

stack work1 ... workN xsetjmp
stack xlongjmp

xsetjmp()はスタックのwork1...N領域をバッファ上にコピーしてからsetjmp()を呼び出す。これでスタックフレームを保存することが出来そう。
xlongjmp()はバッファのwork1...N領域をスタック上にコピーしてからlongjmp()を呼び出す。これでスタックフレームを復帰することが出来そう。

そうは甘くない。

上の手法で継続の復帰をしようとしても上手く行かない。何故かというとコピー先とxlongjmpの作業領域が重なるためにxlongjmp()に失敗するからだ。
問題が作業領域の重なりならば自分で作業領域を作れば良い。例えば、無駄な再帰呼び出しを繰り返すなり、alloca()を使って生成するなり、といった手段がある。

stack work1 ... workN xsetjmp
stack xlongjmp
stack padd1 ... paddN memcpy
stack work1 ... workN longjmp

〆。

一応、POSIX全般で動くはず。sigsetjmpの方が良いかも。

C言語で多値返却

ちょっと古いけどhttp://d.hatena.ne.jp/yuyarin/20090825/1251136545の話。

C標準では。

C言語の仕様レベルでは多値返却にもっとも近いのは構造体の値返却だ。複数の値を返すという用途は充分に満たしている。

struct int_int_t {
  int x;
  int y;
};
struct int_int_t func(void);

しかし、構造体の値返却は同じ構造体を用いて受け取らなくてはならない。例えば

int main() {
  int x;
  int y;
  <x, y> = func();
  ...do somthing...
  <y, x> = func();
  return 0;
}

という処理はC言語には存在しない。

ゼロ・オーバーヘッド。

受け取る側が構造体ならアドレスを受け取ってmemcpy()するだけで済むが、受け取る側が変数二つからなる多値の場合には返り値を二つの変数に分けてコピーする必要がある。C言語の基本機能にはそのようなものがない以上、今から標準に入ることは期待し辛いのではないかと思う。
しかし、C++では非PODクラスの代入ではメンバごとの代入が行われる。つまり、非PODクラスとして多値を実装するのはゼロ・オーバーヘッドとして許されるのかもしれない。

というわけで実装してみた。

#include <stdio.h>

template <typename X, typename Y>
struct Values {
  struct Ret {
    X const x;
    Y const y;
    Ret(X const &_x, Y const &_y)
      :x(_x), y(_y) {
    }
  };
  struct Ref {
    X &x;
    Y &y;
    Ref(X &_x, Y &_y)
      :x(_x), y(_y) {
    }
    Ref& operator=(Ret const& ret) {
      x = ret.x;
      y = ret.y;
    }
  };
};

Values<int, int>::Ret func() {
  return Values<int, int>::Ret(1, 2);
}

int main() {
  int x;
  int y;
  Values<int, int>::Ref(x, y) = func();

  printf("%d, %d\n", x, y);
  
  return 0;
}

「(正しく)動けば良い」

「動けば良い」と言う人はエラーが起きてから「動かないじゃないか」と文句を言う。仮令、その人の操作が間違っていたとしても言う。ある「動けば良い」と言う人が酷いバグで右往左往しているのを見て(少なくともその人の)原因に思い当たった。
その人はコードをデバッガで追うのが大好きだ。で、どこでSEGVが出たかを突き止めて、SEGVの箇所にエラーチェックを追加して修正完了とする。その結果、次々と別の箇所でSEGVが発生したり、エラー時のリカバリに失敗したりしている(ここで右往左往)。
NULL参照のエラーが出たということは、NULLであるべきでない変数にNULLが入っている、という状態になっているはずだ。この場合、NULLが代入された変数を参照した箇所は落ちるべくして落ちたに過ぎない。本当のバグはNULLであるべきでない変数にNULLを代入した箇所にある。
つまり、変数が取り得る値は何か、という観点が抜けている。もちろん、状態遷移図を描いたとしても、それぞれの状態のときに変数の値がどうなっているべきか、という点が定まらない。その結果、設計と実装とが完全に独立してしまう。動くわけがない。
「動けば良い」というのは「(正しく)動けば良い」であって「(間違っていても)動けば良い」ではない。「(正しく)動いている」と「(間違って)動いている」とを区別しない限り「動けば良い」と言うことには意味がない。

〆。

実はhttp://d.hatena.ne.jp/kilrey/20090910#p1の続きだったりする。

問題はエンコーディングではないのだ。

http://itpro.nikkeibp.co.jp/article/COLUMN/20090208/324377/?ST=security&P=1からhttp://d.hatena.ne.jp/tohokuaiki/20090910/encodingの話。

ベースライン。

Webアプリケーション、ここでは一般的な用法として次の条件を満たすもの、の話に限定して進めよう。

  • サーバ・クライアント型のアプリケーションである。
  • http経由で通信する。

サーバ・クライアント型。

間に通信層が挟まっている以上、通信層の信頼性を考慮しなくてはならない。
限定少数の利用者しかいない有線LANならば、パケット化けは起こるとしても、意図的な攻撃を想定する必要はないかもしれない。利用者が増えるにしたがってワームに感染したPCが混じっている可能性も高まるだろう。無線LANならば管理者の気付かぬうちに利用端末が増えている可能性も考慮した方が良いだろう。この辺りの管理は悩みどころだ。
翻ってインターネットを経由する場合は悩むまでもない。潜在的な攻撃者がいることが前提であり、攻撃に晒されることを想定して開発する、という以外の選択肢はそもそもあり得ない。

http経由。

httpのGETやPOSTメソッドを用いた通信が主流だ。さて、GETやPOSTによって送られるパラメータは何者だろうか?
パラメータは決して文字列ではない。それどころかバイト列ですらない。答えはオクテット列だ。

メッセージ {message}
section 4 にて定義される構文を持ち構造化されたオクテットシーケンスから成り、接続を介して転送される、HTTP 通信での基本単位。
http://www.studyinghttp.net/rfc_ja/rfc2616より引用。

つまり。

ただのオクテット列をそのまま文字列として扱えば不都合が出るのは当然だろう。ここでしている操作はC++で言えばreinterpret_castなのだから。もしオクテット列が想定している文字エンコードに合致していたとすれば、それは単に幸運だったのだと思うくらいでちょうど良い。

len = fread(buff, sizeof(buff[0]), buff, fp);
str = reinterpret_cast<LPTSTR>(buff);

〆。

で、対策は次の通り。要するに「自分が欲しいもの=文字列」と「実際に受け取るもの=オクテット列」を区別すれば良い。

  1. 文字列を期待しているパラメータは、最初にオクテット列から文字列への変換を行う。
  2. 文字列の処理はそのエンコーディングに対応した文字列処理関数だけを使う。

「名は体を表す」とは限らない

名と実の関係は極めて厄介なものだ。全く関係がないということはないものの、どちらがどちらに影響を与えているかも判りにくいし、正の相関であるとも限らない。

完成された何かに対して後から名前を付ける。

この場合は「名は体を表す」ことが多い。というのも、その名前はその実体を特定の側面から表現したものとして付けられるからである。
もちろん、命名者の着目した側面が他の者の着目しがちな側面とずれている場合は理解しがたい名前となることもある。それでも、どのような側面から見た名前なのか、を知れば納得できないこともない。命名当時は適切だったが状況の変化により不適切なものになってしまった名前も多い。
このような命名は「形容」に基づくものと言える。その「形容」が適切なものかどうかは別とすれば「名は体を表す」のも当然だ。

未完成な何かに対して先に名前を付ける。

この場合は「名は体を表す」とは限らない。というのも、その名前はその実体への願望や期待を特定の側面から表現したものとして付けられるからである。
もちろん、命名者の願望や期待の内容が他の者のものとずれている場合は理解しがたい名前となる。さらには、命名者の「他の者にこのように理解してもらいたい」という願望や期待が込められている場合には現状から外れる度合いは大きくなるのではないか。
このような命名は「呪術」や「政治」に基づくものと言える。名前がその実体に及ぼす影響というものは全くないわけではない。少なくとも、その実体に関わる周囲の人間に及ぼす影響は大きいだろう。

〆。

面白いのは「呪術」に基づく名前であっても「名は体を表す」ことがしばしばある点だ。
例えば、目標として掲げている内容を達成できるような施策を実際になしている場合、次第に「名は体を表す」ようになるだろう。これは「呪術」よりも施策の成果だと解釈するのが妥当だと思われるが、しばしば「呪術」の成果であるかの如く語られるようだ*1
逆に「政治」に基づく名前では「名は体を表す」ことは少ない。
例えば、

「民主主義」と名乗る国は民主主義ではない。

といったジョークがある。これは単なるエスノジョークだが、

「民主主義」と名乗る(余所の)国は(我々の)民主主義ではない。

という語を補えば、隠れていた話者の価値観が明瞭になるだろう。そもそも政治の層においては、客観的な実体よりも主観的な評価の方が重視される、というだけのことだ。

*1:『人』という字は人と人が支えあって〜。

今の環境ならコンパイル・アセンブル・リンクを分ける必要はない。

C言語の処理過程を考えると大まかに次のようになっている。

  1. 人間の頭
    • 編集
  2. ソースコード
    • パース
  3. 抽象構文木
  4. アセンブリコード
  5. オブジェクトコード
    • リンク
  6. 実行ファイル
    • OSによる起動
  7. プロセス
    • プロセス実行
  8. 出力

これは必ずしも一斉に処理する必要はないのだろう。

例えば。

C言語スクリプト実行系を考えてみる。

  • 処理系の本体は実行ファイルとして持つ。
  • 一部のモジュールはオブジェクトコードとして持つ。
  • 一部のモジュールはソースコードとして持つ。

処理系の本体にtccコンパイラとダイナミックローダを搭載する。LoadModule()を呼ぶと、ソースコードを自動的にコンパイルしてロードする、という具合。コンパイラをライブラリとして持つことができるなら自然な発想だと思う。

〆。

ソースコードこそが一次データであり、実行ファイルさえ単なるキャッシュにすぎないとみなすことができる。極端なことを言えば間に通信を挟んでも構わないし、最適化情報のフィードバックを受けても構わない。
早すぎた最適化に陥らないために。