Gauche:Windows/VC++:log:detail_2005_2

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 のヘビー・ユーザーでないので、使ってみて分かる、移植の見落としが多く残っていると思います。何か問題がありましたら、遠慮なくここに書き込みしてください。

オリジナル版のライセンスに従い配布しています:

LGPLのもと配布しています:

全て、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版ではならない。設定がどこにあるか不明(;_;))。現在ダウンロードできるアーカイブには、問題はありません。

主な進展

その他、ビルド環境としての出来も、徐々に改善していきます。

テスト状況

○=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

これも、限定的な移植です。

以上、不完全な移植ですが、子プロセスを起動し(標準入出力ポートのリダイレクトあり/なし)、終了を待つ、というような基本的な作業は行うことが出来ます。

改善:内部コマンドの実行

 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))))

改善: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の方に反映できるかなあ。

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/01 00:20:12 PDT): 上の「改善:内部コマンドの実行」についてですが、 sys-fork-and-exec等の低レベルAPIは、なるべく生のAPIに近い (Schemeレイヤでは生のAPIに対して必要最低限の加工しか行わない) ということを意図しているので、便利な機能はなるべく上位のAPIに入れて もらえると嬉しいです。(例えばsys-systemやgauche.processで文字列を コマンドとして渡す場合は、UNIXでもシェル経由になるとドキュメントしてあり、 組み込みコマンドが使えることになります)。

まあどこまでやるかに明確な境界があるわけではないので、 ケースバイケースで判断することになりますが。

Windows環境をすぐに立ち上げられないんで確かめられないんですが、 例えばこういう状況だとどうなりますか?

低レベルAPIで余分な処理を避けるという方針にしているのは、既に低レベルAPIの 動作を知っている人を対象としている (従って、余分な処理があると余分に覚えな ければならない)ということ、それからそういう処理によってバグやセキュリティホールが 隠れてしまう場合があることです。後者については、低レベルAPIをそのまま 使う場合はあまり危険はないんですが、その上にたくさんのレイヤを作りこんで いった場合に、低レベルAPIでの特別な処理が暗黙のうちに上位レイヤに持ち込まれて しまうと危険になります。


Last modified : 2012/02/07 08:59:27 UTC