Gauche:Windows/VC++:log:detail_2005_2
過去:Gauche:Windows/VC++:log:old_2003, Gauche:Windows/VC++:log:old_2004
0.8.3 移植第2版
千代郎(2005/04/30 22:13:06 PDT)
前回分かった問題点を修正した移植第2版が出来ました。また、今回は、VC++ で最適化ビルドした、すぐに使える Gauche for Windows (VC++版)も用意しています。 私自身が Scheme のヘビー・ユーザーでないので、使ってみて分かる、移植の見落としが多く残っていると思います。何か問題がありましたら、遠慮なくここに書き込みしてください。
オリジナル版のライセンスに従い配布しています:
- Gauche-0.8.3w-02.zip (開発者用ソース, 2949 kB) - コンパイルには、VC++7.0または7.1が必要です。cygwin環境は必要ありません。
- Gauche-0.8.3w-02-bin.zip (最適化コンパイル済みバイナリ+ライブラリ, 1335 kB)
LGPLのもと配布しています:
- qdbm-1.8.21-for-gauche.zip(ビルド用セット, 401 kB)
- qdbm-1.8.21-for-gauche-bin.zip (QDBMのラッパー gdbm.dll, 75 kB)
全て、http://www.geocities.jp/chiyorou2005/Gauche/ に置いています。
著作権情報については、各アーカイブの文書を参照してください。
※お詫び:5/1の夜から、5/2の昼ごろにかけてアップロードしていた Gauche-0.8.3w-02.zip (3,018,590 byte)には、空のフォルダがアーカイブに含まれていないという問題があり、一部の外部ライブラリのビルドに失敗します。その間にダウンロードされた方は、lib/sxml および、ext/uvector/gauche の2つのフォルダを作成してから、ext以下をビルドしてください。OSの、「圧縮(zip形式)フォルダ」の機能を使ってアーカイブを作成していましたが、この設定により、問題が生じたようです(2台あるパソコンのうち、Windows XP Home版ではこのようになるが、Professional版ではならない。設定がどこにあるか不明(;_;))。現在ダウンロードできるアーカイブには、問題はありません。
主な進展
- 新たに io.scm, rfc.scm のテストをパスするようになりました。
- dbm.gdbm が使用できるようになりました(要 QDBM)
- GNU getopt の代わりに、freegetopt を使用するようにしました。
- MinGW 版に従い、*load-path* などを実行時に取得するようにしました。
- 手軽に機能をテストできるように、すぐに使えるバイナリ版を用意しました。
- プロセス関連の移植をいくつか行いました(下記参照)。
その他、ビルド環境としての出来も、徐々に改善していきます。
テスト状況
○=pass, 成功
△=途中でエラー
ライブラリ(.scm)
system.scm △
selector.scm △
www.scm △
load.scm △
logger.scm △
process.scm △
file.scm △
rfc.scm ○
srfi.scm ○
io.scm ○
primsyn.scm ○
error.scm ○
module.scm ○
macro.scm ○
number.scm ○
string.scm ○
keyword.scm ○
hash.scm ○
procedure.scm ○
dynwind.scm ○
object.scm ○
exception.scm ○
colseq.scm ○
regexp.scm ○
mb-chars ○
parseopt.scm ○
parameter.scm ○
hook.scm ○
text.scm ○
gettext.scm ○
util.scm ○
match.scm ○
io2.scm ○
version.scm ○
listener.scm ○
外部ライブラリ(.DLL)
auxsys △
uvector ○
binary ○
vport ○
mt-random ○
charconv ○
digest ○
sxml ○
dbm(dbm.gdbmのみ)○
子プロセス起動関連の移植+α
千代郎(2005/04/30 22:13:06 PDT)
VC++版では、POSIX関連の機能がほとんど移植されていません。また、完全に移植されることも、ないでしょう(必要なら、cygwin版を使えば良い)。しかし、実用となるには、子プロセスを起動し、処理を行わせるぐらいの機能は欲しいものです。そこで、以下の関数を移植しました。また、使い勝手をよくするために、Windows用の便利な仕組みを追加しました。
sys-exec, sys-fork-and-exec, sys-waitpid の移植
sys-exec
sys-fork-and-exec
これらの関数では、iomap オプションも移植されています。 ただし、sys-fork-and-execでは、下表のように、完全なサポートではありません。
iomapの組み合わせ:
tofd | from-port-or-fd | サポート |
標準入出力 | 標準入出力 | ○する |
標準入出力 | その他 | ○する |
その他 | 標準入出力 | ×しない |
その他1 | その他2 | ×しない |
その他1 | その他1 | ○する |
sys-waitpid
これも、限定的な移植です。
- 特定のプロセス(pid > 0)しか待つことができません。
- untracedオプションをサポートしていません。
- 戻り値のうち、終了ステータスには、終了コードのみ入り、sys-wait-exited? は常に #t を返します。
以上、不完全な移植ですが、子プロセスを起動し(標準入出力ポートのリダイレクトあり/なし)、終了を待つ、というような基本的な作業は行うことが出来ます。
改善:内部コマンドの実行
Windowsのコマンド・ライン環境が、UNIXシェルと異なる点は、(cd,嘘) mkdir, echo など多数の基本コマンドが、Cmd.exe (または、Command.exe)の内部コマンドである点です(つまり、mkdir.exe のような、単体の実行ファイルはどこにも存在しまぜん)。
従って、本来なら、例えば dir (= UNIX の ls に相当)コマンドを子プロセスとして実行したければ、
(sys-fork-and-exec "cmd" '("cmd" "/C" "dir" "/B"))
としなければなりません。
これでは不便なので、内部コマンドを Gauche 内で変換し、単純に、
(sys-fork-and-exec "dir" '("dir" "/B"))
とできる仕組みを作りました。
実用サンプル:カレント・ディレクトリの内容を、dir.out に書き込む。
(let ((out (open-output-file "dir.out"))) (sys-exec "dir" '("dir" "/B") `((2 . 1) (1 . ,out))))
- cd は UNIX でもシェルの内部コマンドです。
そもそも、子プロセスとして cd を実行しても意味が無いと思います。
- (千)意味がない←確かに、そうですね。訂正しておきました。また、ここに書いてある「改善」は、議論のところにあるように、すでに問題が多く、廃止予定です。
改善:WSHスクリプトの実行
UNIXシェルでは、ファイルの先頭に #!/bin/sh のように書き、さらに実行可能にすることで、スクリプト・ファイルを直接実行できます。 一方、Windowsでは、実行可能かどうかは拡張子で判断され、バッチ・ファイル(.bat)や WSHスクリプトが、直接実行できます。しかし、WSHスクリプトの場合、規定の設定では、出力が、メッセージボックスに表示されてしまいます。そこで、WSHのサポートするスクリプト・ファイルが常にコマンドラインを出力とするような仕組みを作りました。
実用サンプル:Gaucheのバージョンを表示する。
(sys-fork-and-exec "../win_utils/gauche-config.js" '("../win_utils/gauche-config.js" "-V"))
まとめ
Gauche Windows環境において、WSHスクリプト(JScript・VBScript)/バッチ・ファイル/コマンドを組み合わせ、複雑な仕事ができるようになります。
実用サンプル:set コマンド(Unix の env)による環境変数一覧をファイルに保存し、「メモ帳」で開く(run-process は、内部で、今回移植した3つの関数を使用します)。
(use gauche.process) (begin (run-process "set" :output "set.out" :wait #t) (run-process "notepad" "set.out"))
テスト失敗の原因追求
千代郎(2005/05/02 06:49:53 PDT)
file.scm
様々な要因がからんで失敗しています。一つ一つ解決していかなければなりません。
まず、directory-list 関係のテストがことごとく失敗してしまいます。 原因を探ったところ、Scm_ReadDirectory() に私が書いた、Windows用の移植コードに間違いがありました。ディレクトリ・エントリを列挙する目的で、
h = FindFirstFile(Scm_GetStringConst(pathname), &wfd); [...] } while(FindNextFile(h, &wfd));
としています。
FindFirst(Next)File API は、その名の通り、引数のパターンに一致するファイル/ディレクトリ名を取得するものです。引数 pathname に、例えば、"Gauche-0.8.3/src" を渡すと、"src" を見つけて終わりになります。src ディレクトリの内容を列挙したければ、"Gauche-0.8.3/src/*" を渡さなければなりませんでした。
opendir, readdir を、単純に、FindFirstFile, FindNextDir で置き換えるという誤りは、 http://home.a03.itscom.net/tsuzu/programing/tips19.htm ここのコード(ひどい間違いです)を盲信したためやってしまいました。反省しています。現在の VC++ 02版は、directory-list を使うコードで、変な挙動を示します。使ってらっしゃる方は、注意してください。
(2005/05/04 00:14:20 PDT)
次に、目立つのが、sys-lstat が unbound であるために生じる失敗です。 VC++では、CRTライブラリにおいて、WinAPIにラッパーをかぶせることで、POSIX準拠のシステム・コールのいくつかを提供してくれています。例えば、chmod(), stat() があります。しかし、中途半端なところが、いやなところで、lstat(), fstat() はありません。また、stat()は、あるにはあっても、戻り値の stat 構造体のメンバのほとんどに、意味のある値が入っていません(後述)。
まず、lstat() がない問題です。Windowsでは、「再解析ポイント」という属性をつけることで、シンボリック・リンクを作ることができます。しかし、ファイルに対する再解析ポイントの設定が、初期状態では出来ないようになっている(ディレクトリに対してはできる)ので(参考文献: http://homepage1.nifty.com/emk/symlink.html )、とりあえず、Gauche VC++版では、シンボリック・リンクは扱わないことにします。lstat_() を、「再解析ポイントが設定されているパスを与えると、エラー、その他は、stat() に委譲」という方針で実装することにしました。
これでエラーが減りますが、それでも sys-lstat を使う、以下の関数のテストに失敗します。
file-eq?, file-eqv?, file-equal?
これらの lib/file/util.scm の関数は、stat構造体のうち、dev および inoメンバを参照し、「デバイス番号と、inode 情報が一致していれば、ファイルを同一とする」という実装になっています。しかし、VC++ CRTライブラリの ino には、常に 0 が入り、これでは、全てのファイルが同一ということになってしまいます。
ちなみに、VC++版 stat で、メンバとしてはあっても、意味のある値が入らないものは以下の通りです(ソース ・コードで確認しました):
ファイル・システム | NTFS | FAT |
st_gid | × | × |
st_atime | ○ | ×(= st_mtime) |
st_ctime | ○ | × |
st_ino | × | × |
st_uid | × | × |
st_nlink | × | × |
○=値が入る。×=常に同じ値(0 など)
ひとつの解決法は、自前で st_ino に値を入れることで、例えば、cygwinでは以下のように解決しています:
「6 Cygwin の stat の実装」http://www.kaimei.org/note/mag/in_cyg.html
涙ぐましい工夫をして、それでもなお、異なるファイルを同一と判定する可能性の残った実装になっています。
ここでの目的は、cygwinを再発明することではないですし、かといって、俺様仕様の sys-lstat を用意するのは、前回の sys-exec の場合に指摘を受けたように、問題があります。そこで、
lib/file/util.scm を書き換えることで対処すべき、という結論に至りました。
Windowsで、inode 情報に当たるファイルの ID は、GetFileInformationByHandle() API の戻り値 BY_HANDLE_FILE_INFORMATION 構造体の nFileIndexHigh および nFileIndexLow メンバに格納されます。また、デバイス番号は、dwVolumeSerialNumber メンバに入ります。ちょうど、st_dev => dwVolumeSerialNumber, st_ino => nFileIndexHigh および nFileIndexLow という対応になります。そこで、これら、3つのメンバの値が全て等しい場合に、ファイルを同一と判定できます。
判定を行う Scheme 関数 win-is-the-same-file? を、winlib.stub に作成し、core.c に、Scm_Init_winlib(Scm_GaucheModule()) を追加しました。
Function: win-is-the-same-file? path1 path2
そして、これを用いて util.scm のうち、file-eq?, file-eqv?, file-equal? を書き換えました。 (ここで、気になるのは、Gauche のライブラリに、版ごとの違いが出てきてしまうことです。.scmファイルで、C の #if のような切り分けが出来れば、統合できるのですが...。) 以降、Windows専用関数を、winlib.stub に追加していくことにします。
(ところで、genstub を始めて使いましたが、よく出来ています。 C/C++の関数を Scheme に組み込むのが、ものすごく簡単に出来ました。私のようにどちらかと言えば C/C++ が得意な者は、他の Scheme 処理系はもはや使えなくなってしまいそうです。)
ついでに、temporary-directory の定義も Windows 環境になじまないので、GetTempPath() API をラップした win-get-temp-path を作成し、書き換えてしまいます。
ここまでの変更で、エラーは残りわずかになります。
discrepancies found. Errors are: test current-directory: expects ("/" "/" #t #t) => got ("C:/" "C:/" #t #t) test copy-file (:if-exists :supersede, safe): expects #t => got #<error "renaming \"test.out/test.copy77d251\" to \"test.out/test.copy\" failed: File exists"> test copy-file (:if-exists :backup, safe): expects #t => got #f test move-file (:if-exists :supersede): expects #t => got #<error "renaming \"test.out/test5.o\" to \"test.out/test.move\" failed: File exists"> test move-file (:if-exists :backup): expects #t => got #f
最初のものを除き、sys-rename の失敗が原因です。UNIX の rename() は、変更したい名前のファイルがすでに存在する場合には、上書きします。一方、VC++ の CRTライブラリの実装では、rename() は、MoveFile() API で実装されているため、変更先のファイルが存在していた場合に、失敗してしまいます。MoveFile() の拡張版である、MoveFileEx() API では、動作オプションが設定可能で、MOVEFILE_REPLACE_EXISTING フラグを設定すると、上書き動作が可能になります。そこで、MoveFileEx() で実装した、rename_() を作成しました。
これで、残りは1つです。
discrepancies found. Errors are: test current-directory: expects ("/" "/" #t #t) => got ("C:/" "C:/" #t #t)
これはパスの取り扱いに関する違いが原因です。パスを内部でどのように扱うか、私の VC++版の方針は、「パスは、ロング・ファイル名のみ許容。パスの区切りは、'/' に統一する。ドライブ・レター(C: など)は使う」というものです。パスの区切りを '/' に統一するのは、Win API が、'/' もパス区切りとして使えることと、'\' の場合、"C:\\Program Files\\ ..." のように \\ とせねばならず、読み書きがわずらわしいからです。ドライブ・レターを許容するのは、cygwin のように /cygdrive/c/... とするのが、Windows文化(mountという概念がない※)となじまないからです。
※追記:Windows 2000以降, NTFSでは、API による mount のサポートがあります。
上記のテストは、カレント・ディレクトリを、"/" に変更するテストです。カレント・ディレクトリは、確かに、使用中のドライブの "/" に移動していますが、Windowsの場合、"/" は、ドライブごとに存在するので、結果が一致しません。この場合、テスト側を変更すべきでしょうが、かといって、expects ("C:/" "C:/" ... とするわけにもいきません。"A:/" であったり、"D:/" であったり、いろいろな可能性があるからです。そこで、_getdrive() をラップした、win-get-current-drive-root を導入し、結果を比較することにします。将来的には、他のドライブ操作関数も用意すべきでしょう。
file.scm テストの変更:
(test "current-directory" `(,(win-get-current-drive-root) ,(win-get-current-drive-root) #t #t) (lambda () (let* ((cur (sys-getcwd)) (root (begin (current-directory "/") (sys-getcwd))) (root* (current-directory)) (cur* (begin (current-directory cur) (sys-getcwd))) (cur** (current-directory))) (list root root* (string=? cur cur*) (string=? cur* cur**)))))
これで、ようやく全 pass します。この場合、テスト側を(passするべく)変えているので、テストの意味合いが薄れてしまう面もあります。
load.scm
上の、file.scm のための修正によって、何もしなくても、load.scm も pass しました。やった!
コメント、議論
Shiro(2005/05/01 00:20:12 PDT): 素晴らしい。 0.8.4の方に反映できるかなあ。
- 千代郎(2005/05/01 13:23:03 PDT):
そう言っていただけて、嬉しいですが、いろいろ問題がありそうです。
今回の移植の場合、どちらかといえば、Makefileやスクリプトなど、ビルド環境の移植という面が大きく、
関数レベルのパッチと違い、統合するのが難しそうです。nmake を使うために、本来不要だった変更を加えたりと、UNIX環境なしにこだわっている面が大きいです。
しかし、現実的な割り切り方としては、「VC++でコンパイルするが、make に cygwin 環境を前提としてしまう」やり方になるのかもしれません。その場合には、
- system.c に加えた、Scm_SysExec() への変更
- vc-compat.h, vc-compat.cpp にあるいくつかのサービス関数
- など、利用していただけたら、幸いです。しかし、VC++版を追加するためには、むしろ、以下のような大枠のことが、問題となるのではないでしょうか。
- ext/ におけるDLLエクスポートの問題(なんとか自動化できないか? xxx_head.c, xxx_tail.c みたいな仕組みを利用できないか? 考え中です。)
- #ifdef __MINGW32__ と、VC++版との切り分けが、複雑になり、保守しにくくなるのでは(ときどき、#ifだらけのつぎはぎのソースを見ますが、よくあんなものが見落としなく維持できるなあと思ってしまいます。何かエディタの支援があるのでしょうか。)
SCM_SIGCHECK
千代郎(2005/05/02 06:49:53 PDT):
Shiro様
いきなり質問ですいません。教えていただきたいのですが、
ディレクトリ・エントリを列挙する目的のコードで、
2箇所にある、SCM_SIGCHECK(vm) は、どういう目的のものですか?
system.cより
struct dirent *dire; DIR *dirp = opendir(Scm_GetStringConst(pathname)); if (dirp == NULL) { SCM_SIGCHECK(vm); Scm_SysError("couldn't open directory %S", pathname); } while ((dire = readdir(dirp)) != NULL) { ScmObj ent = SCM_MAKE_STR_COPYING(dire->d_name); SCM_APPEND1(head, tail, ent); } SCM_SIGCHECK(vm); closedir(dirp);
- Shiro(2005/05/04 16:55:08 PDT): シグナルの処理を行うマクロです。 シグナルハンドラ中で行える処理はOS的にひどく限られていて、例えばSchemeで定義した シグナル処理ルーチンをシグナルハンドラ内で走らせる、というようなことは できません。従ってGaucheでは、シグナルハンドラで受けたシグナルをキューに入れ、 シグナル処理を行っても安全な箇所でキューをチェックしてシグナル処理を行っています。 SCM_SIGCHECK()はキューのチェック+シグナル処理を行うマクロです。 通常は、VMのループ内でSCM_SIGCHECK()を行っているのであまり気にする必要は ないのですが、Cのルーチン内でループしてVMにしばらく制御を返さないような場合には 適宜SCM_SIGCHECKを挿入しておかないと、シグナルが思ったように処理されません (例えば上記ルーチンでSIGCHECKが無いと、ディレクトリを読んでる間、^C等が効かなく なります。巨大なディレクトリを読みはじめて中断したい場合なんかに困りますね。)
- 千代郎(2005/05/05 20:23:47 PDT): よく分かりました。ありがとうございます。なにか、
ディレクトリ・エントリ固有の問題かと思って頭を捻っていましたが、もっと、
全体の設計にかかわることだったんですね。以下のような問題への対処であると
理解しました。移植コードでも気にするようにします。
- Schemeで定義した処理をシグナル・ハンドラとするためには、VMが特定の状態のときに、シグナル処理を行う必要がある。
- ゆえに、シグナルの処理を必要な瞬間まで遅延する必要がある。
- しかし、こうすると、シグナル本来の目的である「割り込み」が効かない期間が生じてしまう。
「改善:内部コマンドの実行」について
Shiro (2005/05/01 00:20:12 PDT): 上の「改善:内部コマンドの実行」についてですが、 sys-fork-and-exec等の低レベルAPIは、なるべく生のAPIに近い (Schemeレイヤでは生のAPIに対して必要最低限の加工しか行わない) ということを意図しているので、便利な機能はなるべく上位のAPIに入れて もらえると嬉しいです。(例えばsys-systemやgauche.processで文字列を コマンドとして渡す場合は、UNIXでもシェル経由になるとドキュメントしてあり、 組み込みコマンドが使えることになります)。
- 千代郎:(sys-fork-and-exec "ls" ... というサンプルがあったので、これと同じように使えたらいいなあ、程度の考えでした。 下位APIに機能を詰め込むのはまずいかと、ちらと頭を掠めたのですが、 Schemeの中間レイヤを書く技量がなかったため、移植ついでに盛り込んでしまいました。
まあどこまでやるかに明確な境界があるわけではないので、 ケースバイケースで判断することになりますが。
Windows環境をすぐに立ち上げられないんで確かめられないんですが、 例えばこういう状況だとどうなりますか?
- (sys-fork-and-exec "dir" '("dir" "%HOME%")) とか、cmd.exeが
解釈してしまいそうな文字列が引数に入ってたらどうなるか。
sys-execのセマンティクスはシェルによるメタキャラクタの解釈を行わない exec(3) の
APIを反映しているので、勝手に余分な解釈が行われると困ることになります。
(プログラマが自ら"cmd"を指定していた場合はわかってやっているのでよい)。
- (千)環境変数の展開に関しては、cmd規定の設定では、しないようになっています。しかし、ワイルド・カードの展開は、やってしまうので、 (sys-fork-and-exec "dir" '("dir" "*.c")) などで、問題になります。このような問題は、ちらとも思いつきませんでした。
- Windowsってdir.exeってのがあった場合、システムコール経由でもそれを
実行することはできないんでしたっけ。もしシステムコール経由ならdir.exeが
実行できてしまう、というのなら、プログラマがそれを意図していた場合に
動作が変わってしまいますね。
- (千)dir.exeは実行できるので、意図とは変わってしまう可能性があります。
低レベルAPIで余分な処理を避けるという方針にしているのは、既に低レベルAPIの 動作を知っている人を対象としている (従って、余分な処理があると余分に覚えな ければならない)ということ、それからそういう処理によってバグやセキュリティホールが 隠れてしまう場合があることです。後者については、低レベルAPIをそのまま 使う場合はあまり危険はないんですが、その上にたくさんのレイヤを作りこんで いった場合に、低レベルAPIでの特別な処理が暗黙のうちに上位レイヤに持ち込まれて しまうと危険になります。
- (千)確かにまずいですね。次の版では、改めます。