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をコンソールアプリとして生成するとこまではできてます。
- ext/* 以下の拡張モジュールはまだ
- システム関係はいろいろ未サポート
- win64では動かない
です。特にsys-globについては現在のCVS版はbrokenですが、全面的に書き直し中 なのでお待ちあれ。
CVSからのコンパイルはちょっと面倒で、「VC++でコンパイル可能なソースツリー」を 生成するために「cygwin + 最新版gaucheのインストールされた環境」が必要です。 ソース中のファイルHACKINGを参照してください。 (正式リリースの時には、プリプロセスしてVC++単独でコンパイル可能にした ソースツリーを用意します)。
(2007/08/27 02:59:21 PDT) ext/* 以下のコンパイルに関して:無理矢理通そうと思えばたぶん通るんですが、
- なるべくMakefileと同じ情報を重複して持ちたくない (WSH経由でgenstubを呼び出す とかだと stub -> c のルールがMakefileとWSHで重複するので避けたい; WSHで Makefileをパーズするとこまでやれたら別だけど)
- *_head.c, *_tail.c は要らないかもしれない (ずっと昔のBoehm GCのバージョンで、 特定のプラットフォームでdata, bssエリアがうまく検出できない対策で入れたんだけど 現在のバージョンがVC++でどう動作するか要調査)
- ext/ 以下を増やしてゆく際に、最小限の労力でVC++サポートも増やせるようにしたい (Unixビルドで、共通部分がMakefile.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扱いに ならないかな?
- Shiro(2007/09/12 04:11:25 PDT): 思い出したッ! Cygwin supportを開始した当初、
その頃のgccは現在のVC++のように外部DLLのデータアドレスをコンパイル時定数として
扱えなくて、そのためのハックをちゃんと入れていたんだった。
http://gauche.cvs.sourceforge.net/gauche/Gauche/src/gauche.h?revision=1.287&view=markup
256行目あたりから。
タイプタグがクラス構造体を直接指すのでは無く、一段間接参照を噛まして指すようにして、 静的初期化はインポートテーブルのアドレスで行うというわけ。 型の比較に一回メモリアクセスが増えるのでペナルティがあるけれど これは使えるかもしれないッ!
消え去ったコードが5年の歳月を経て復活しようとしているッ!
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できなく なるわけですが。