Gauche:Windows/VC++:log:old_2007

Gauche:Windows/VC++:log:old_2007

Gauche:Windows/VC++のログ、2007分


Shiro (2007/08/26 17:45:00 PDT): 千代郎さんの書き込みからだいぶん放置してしまって 申し訳なかったのですが、ようやくソースツリーにてVC++版への対応を開始しました。 0.8.12か0.8.13くらいに含められたらいいなと思っています。

現在のところ、goshをコンソールアプリとして生成するとこまではできてます。

です。特にsys-globについては現在のCVS版はbrokenですが、全面的に書き直し中 なのでお待ちあれ。

CVSからのコンパイルはちょっと面倒で、「VC++でコンパイル可能なソースツリー」を 生成するために「cygwin + 最新版gaucheのインストールされた環境」が必要です。 ソース中のファイルHACKINGを参照してください。 (正式リリースの時には、プリプロセスしてVC++単独でコンパイル可能にした ソースツリーを用意します)。

(2007/08/27 02:59:21 PDT) ext/* 以下のコンパイルに関して:無理矢理通そうと思えばたぶん通るんですが、

といった点を考慮したいと思っています。

DLLとタイプタグ

Shiro(2007/09/09 04:46:46 PDT): WindowsのDLLの仕様はぶっ壊れてる ←結論。
gcc/mingwとgcc/cygwinはそれでも穴を塞ごうと努力してるがVC++はもう救いようがない。

dataやbssに置かれる変数のアドレスは定数である、ということを利用して、 Gaucheはオブジェクトのタグをクラス構造体のアドレスから算出している。 ランタイムにクラスを「登録」しなくても、クラス毎にユニークな値が確実に 得られるから。ところがWindows DLLでは、dllimportした変数参照は コンパイラがポインタ間接参照に読みかえるようになっている。そして、 dllimportした変数のアドレス式は、ポインタの値を得る式へと読みかえられるので 定数式にならない。

そう設計した理由はわからないでもない。コンパイラのほんのちょっとしたハックで、 リンカの仕事がうんと減るから。けれど、これはleaky abstractionだよなあ。 実装の事情を隠すならちゃんと最初から最後まで隠して欲しいわけだ。

gccはがんばっていて、他のDLLにある変数でも、(dllimport宣言をしない、ただの externで参照している場合は) 動的リンク時にちゃんとアドレス計算をやってくれる。 (SCM_CLASS_DECLがSCM_EXTERNではんなくexternを使っていたのはそのため)。 だがVC++ではdllimportは必須のようだ。

VC++でも、/TPオプションでC++としてコンパイルすれば単なるアドレス式なら 通ったらしい (Gauche:Windows/VC++:log:detail_2005)。 static constructor扱いになっていたのかな。 確かこの頃の実装はクラス構造体のアドレスがそのままタグだったからなあ。 今はアドレスの下位2bitを1にしたのがタグになってるんだが、VC++はこの計算を やってくれないようだ (定数式でないとして拒否する)。

このdllexportとdllimportも醜いよなあ。こういうkludgeが推奨されているようだが:

#ifdef FOO_EXPORTS
#define EXTERN __declspec(dllexport)
#else
#define EXTERN __declspec(dllimport)
#endif

これはひどく質の悪いkludgeだ。 dllを複数扱う場合にこの例のEXTERNにあたるシンボルが衝突しちゃまずいから、 モジュール毎にEXTERNにあたるマクロを変えることになる。 そうすると、このイディオム自体をマクロか何かにくるんで抽象化することができない。 dllを書く度に、この醜いイディオムがヘッダに入り込むことになる。 VC++的にはIDEのテンプレートで自動生成するからいいって思っているのかもしれないが、 それがIDE脳の恐怖ってやつだ。

プログラムは人が読むために書かれるべきであり、 それがたまたま機械でも実行できるというだけにすぎない。

コードの自動生成は書く手間を減らすかもしれんが、読む手間をかえって増やしてしまう。 定型的なイディオムならば、それをひとことで表現できる「言葉」を定義することで 抽象化すべきだ。それがプログラミング言語の役割なんだから。 抽象化を許さないシステムは、 プログラマから良いコードを書こうというインセンティブを奪ってしまう。 そんな環境で長くコードを書きつづけると、脳の前頭前野に不可逆的なダメージを 負ってしまうに違いない。あなおそろしや。

こんなシステムに合わせるために設計を変更するのは敗北なので なんとか他の手を考えてみる。タグをクラスとして宣言して コンストラクタで値を算出するようにしたらstatic constructor扱いに ならないかな?

cmd.exeと文字コード

sakiyama (2007/08/29 03:28:59 PDT): utf-8ビルドされたgoshで、cmd.exeコンソール上に書き出すときには、WriteConsoleWを使う必要があるようです。 http://forums.belution.com/ja/vc/000/374/95.shtml

file_flusherのwriteを下記のものにすると、コンソール、リダイレクトで表示できました。動作確認用で無理矢理です。

int win32_write( int fd, const void *buffer, unsigned int count )
{
  int written;
  HANDLE h = (HANDLE)_get_osfhandle(fd);
  wchar_t* w;
  int size;

  if (WriteConsoleW(h, NULL, 0, &written, NULL)) {
    /* console */
    size = MultiByteToWideChar(CP_UTF8, MB_ERR_INVALID_CHARS, buffer, count, NULL, 0); 
    w = (wchar_t*)malloc(sizeof(wchar_t) * size);
    MultiByteToWideChar(CP_UTF8, 0, buffer, count, w, size); 
    WriteConsoleW(h, w, size, &written, NULL);
    free(w);
  } else {
     /* redirect */
    _write(fd, buffer, count);
  }
  return count;
}

コンソール上から日本語を入力すると、SJISで扱われているようでさらに化けるので

(use file.util)
(write (file->string "utf8.txt"))

とやって確認しました。

Shiro(2007/08/29 03:57:13 PDT): なるほど。 コンソールを「特殊なデバイス」とみなすならデバイスに一番近いところ (システムコールwriteのラッパー) として対応を仕込むのは理にかなっている とも考えられますね。

ただ、write()に渡されるのは「utf-8オクテット列」ではなくてあくまで 「バイナリデータ」なんですよね…たまたまそれがutf-8になっている場合もある、 っていうだけで。具体的に問題が出そうなケースとしては、一文字を構成する utf-8オクテットが複数のwriteコールに分断されてしまうといったものが 考えられます。writeレベルで対応を入れるなら、その中でさらに バッファリングしなくちゃならない (渡されたデータの末尾に不完全な utf-8列があった場合、それを保存しといて次に来たデータと合わせる必要がある)。 バッファリングが複数レイヤで行われるのはちょっと危うい感じがします。

プログラム実行中に出力先が変更になってしまうことは無いんですよね (unixなら close/dupで標準出力を途中ですげかえることができますが、Windowsはどうなんかな)。 変更が無いのなら、起動時に出力がコンソールであることを検出したら標準出力ポートを 特別な「コンソール出力ポート」にしてしまう、って手があるかもしれません。

sakiyama (2007/08/29 22:09:32 PDT): どこに仕込むべきか判断付かなかったので、一番下でやっちゃいました。コンソール出力ポートが一番良い気がしますが、windowsでもdup,closeはありますので出力先は変更できちゃいます。

Shiro(2007/08/29 23:22:38 PDT): そうか… まあ、dupのSchemeレベルのインタフェースである port-fd-dup!はファイルポートのみが対象なので、プログラム起動時に標準出力が 「コンソール出力ポート」につながれた場合はどちらにせよSchemeからはdupできなく なるわけですが。

More ...