Gauche:Windows/VC++:log:detail_2005
- VC++への移植、新たな試み
- 移植作業メモ:
- configure がらみの移植:
- DLLのインポート/エクスポートの問題:
- ネストした構造体の問題:
- ext の移植で、C++としてコンパイルする必要があった理由:
- 既存のもののお世話になり、うまく解決できた点:
- C++予約語の問題:
- シンボリック・リンクの問題:
- ソースの不具合と思われたところ:
- テスト失敗の原因追究
- コメント、議論
VC++への移植、新たな試み
はじめまして。千代郎といいます。 普段は、C++ がメインです。陰ながらGaucheを応援していますが、Schemeは、 入門書を読んだ程度で、まだまだ実用的に使いこなしているとはいえません。 しかし、C 関係のことなら参加できると思い、Gaucheの最新版 0.8.3 を VC++ で コンパイルできるようにしてみました。
goshをVC++でそのままコンパイルした例がないようなのと、VC++でコンパイル できるようにすれば、開発の輪が Windows 界の人にも、もっと広まっていくのでは、 というのが動機です。
目標:
目標は、とりあえず、コンパイルできて動くこと。 POSIX関連は、ほとんど切り捨て。とにかく動けば、あとから いくらでもよくしていけるという方針。
場所:
ここにおいています。→http://www.geocities.jp/chiyorou2005/Gauche/
すぐにビルド作業を開始できるアーカイブ全体と、Gauche-0.8.3.tgz との差分
を用意しました。
(差分については、手元では、patchを使ってうまく復元できません。差分元に ないフォルダが、うまく作成されないようです。ファイル内容自体は、差分に 含まれているので、なにか方法があるはずですが。UNIXのツールにはあまり詳しく ないので、どなたか詳しい方、お知恵を拝借できませんか?)
ビルド方法:
詳しいビルド方法については、アーカイブを展開した後、win_utils という フォルダにある readme_1st.txt を読んでください。 最適化なしのデバッグ・ビルドになります。ソースを C++ としても ビルドできるように、変更を行いました(この理由については後日書きます)。
VC++の IDE を使わず、全て、コマンド・ラインで作業を行うので、
マイクロソフトがフリーで公開しているVC++コンパイラ
Microsoft Visual C++ Toolkit 2003
http://msdn.microsoft.com/visualc/vctoolkit2003/
があれば、コンパイルできると思います(試してはいません)。
- 2005/04/16 08:19:01 PDT 追加:WinAPIを使用するため、Platform SDK(フリー。ただし、でかい)も必要です。
現時点の結果:
テスト
○=pass, 成功
△=途中でエラー
×=gosh.exeがクラッシュ
<Gauche>
system.scm ×
selector.scm ×
www.scm ×
rfc.scm △
io.scm △
load.scm △
logger.scm △
process.scm △
file.scm △
vm-stack ○
test-arith ○
srfi.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 ○
symcase.scm ○
<ext>
auxsys △
uvector ○
binary ○
vport ○
mt-random ○
charconv ○
digest ○
sxml ○
※残りはまだ作業を行っていない。
おわりに:
このバージョンには、Gauche:Windows/VC++:log:detail_2005にあるように、すでに様々なバグが見つかっています。
移植作業メモ:
千代郎
今回の作業中、問題になった点について、まとめてみたいと思います。
私の思い間違いの部分もあるかもしれません。遠慮なくコメントお願いします。
(Shiroさん、はじめまして。整理ありがとうございます。しかし、またごちゃごちゃしちゃいました。)
configure がらみの移植:
cygwinを導入していない場合、Windows では、autotools や、シェル・スクリプトが動きません。そのため、configure で設定される項目の作成が問題になります。Windows環境では、Cヘッダーの有無について、大きな差異はなく、また、ディレクトリ構成(アプリケーションの置き場)がそれほど問題にならないので、configure の必要性を余り感じません。そこで、configure は、設定ファイルをもとに、テキスト中の @???@ という変数を置換するものだと割り切り、そのような処理をするスクリプトを、JavaScriptで用意しました。スクリプトは、Windows Scripting Host (WSH)でコマンドラインから起動することが出来ます。WSHは、最近のOSでは添付されており、また、フリーでダウンロードすることもできるため、標準環境であると考えました。ビルド作業に必須な gauche-config のようなスクリプトも、JavaScript 版を作成しました。ビルド環境は、VC++の nmake および、JavaScript, バッチ・ファイルの組み合わせで構築しました。
DLLのインポート/エクスポートの問題:
動的にロードされるモジュールについて、UNIXでは、変数は単なる extern で、自動的に解決されるようですが、VC++では、厳密というか、融通が利かず、特にエクスポートは、明示的に指定してやる必要があります。もちろんすでに、SCM_EXTERN というマクロがその解決のために導入されていますが、ところどころで、うまく機能していない場所がありました。
・gauche.h において、
#define SCM_CLASS_DECL(klass) SCM_EXTERN ScmClass klass
とあるべきが、
#define SCM_CLASS_DECL(klass) extern ScmClass klass
となっており、Scm_...Classという変数名がエクスポートされず、libgauche.dll の外部から見えない。
・ext関連について。個々の .dll は、gauche.h 中の変数(libgauche.dllがエクスポートする)を参照します。こちらは、LIBGAUCHE_BODY および、SCM_EXTERN マクロの利用によって、うまくインポート/エクスポートが調整されています。しかし、自身のソース中の SCM_EXTERN(一つ前の項目↑の変更によって生じる。extern ScmClass klass としてあったのは、この問題を回避するためかもしれない。しかし、それはそれで別の問題を生むので、結局、このあと書くようにするしかないと思う) については、LIBGAUCHE_BODYが定義されないため、dllimport になってしまい、変数がエクスポートされません。また、extのDLLが、他のextのDLLから利用される場合(例:uvectorが、mt-randomから使われる)のような複雑な場合もあります。この解決のためには、.../ext の各 .dll ファイルについても、libgauche でしているように、LIBGAUCHE_BODY といういうようなマクロ変数によって、処理を切り替える必要がありました。
- 2005/04/15 10:46:34 PDT追加:MinGW版の開発経緯を知ろうと、devel-jp ML(私は参加していません)のアーカイブを読んでいると、2004年7月のスレッドで、すでに、この項目で書いてることそのままが問題になっていることが分かりました。shiroさんの「一部のアーキテクチャのせいでコードが読みにくくなるのは避けたいので、なんかcleverなトリックはないかしらん」ということについて、考えてみます。
・extから利用される、libgauche中のhook関連の関数名が、extから見える場所で、SCM_EXTERN 宣言されていません。これらの宣言を、gauche.h の末尾に追加しました。
- 2005/04/15 11:08:26 PDT追加:あっ、こっちもMLにありました。「Scm_ReadUvectorHookがgauche.hに書いてないのは意味があります。ちゃんとやるなら、gauche/read.hみたいな別ヘッダにわけることになると思います」。もっと早く目を通しておけばよかった。
・ext では、Scm_Init_...というエントリーポイントが、外部から呼ばれます。しかし、この関数が SCM_EXTERN となっていません。これらの変数については、.defファイルというエクスポート名の設定ファイルで指定しました(もちろん、ソース中に SCM_EXTERN を埋め込むことでも対処できる。)
・builtin-symsで、Scm_BuiltinSymbols が、SCM_EXTERNされたものとして認識されていない。なぜなら、builtin-syms.cに、builtin-syms.hがインクルードされていないから。
ネストした構造体の問題:
VC++では、入れ子のstructの内部structは、外部スコープから見えなくなります。そこで、内部の構造体を、先に、個別に宣言してやる必要がありました。たとえば、gauche.hの、ScmRegMatchSub などです。
ext の移植で、C++としてコンパイルする必要があった理由:
libgauche, gosh については、C としても、C++ としてもコンパイルできます(「C++として」というのは、ソースを強制的にC++ソースと見なしコンパイルすることです)。しかし、../ext 以下のライブラリについては、C++ としてしか、コンパイルできませんでした。理由は、以下の通りです。
各モジュールでは、.stub を変換したソース中で、SCM_DEFINE_... 系のマクロが構造体をstaticに定義することが頻出します。この構造体の初期化リスト中に、{ &Scm_...Class, ... } という項目があります。ここで、Scm_...Class は、libgauche.dll からインポートされる変数です。この場合、&Scm_...Class は、定数であるはずですが、VC++でコンパイルすると、「定数以外を構造体の初期化に用いている」というエラーが出て、なぜかコンパイルできません。しかし、C++として(-TPというオプションをつけて)コンパイルすると、コンパイルすることが出来ます。これは、VC++固有の問題だと思われます。
この状況を再現する短い例としては、以下のソースがあります。
> cl /c test.c
> cl /TP /c test.c
の両者のコンパイル結果を比較してみてください。
test.c
extern __declspec(dllimport) int outer_data; //DLLインポートする外部変数 extern int outer_data2; //ただの外部変数 typedef struct { int *a; int b; }str; static str a = { &outer_data, 0 }; //Cとしてコンパイルすると、ここでエラー static str b = { &outer_data2, 0 }; //こちらは、C or C++どちらでもOK int main(int argc, char *argv[]) { return 0; }
既存のもののお世話になり、うまく解決できた点:
・int64.h関連は、VC++ の __int64, __int32 を利用した。
・/src/gauche/mingw-compat.h をほとんどまる写しして、vc-compat.hを作った。
・getoptは、GNUのものを使った。
- ちゃんと調べていないのですが、GNU の getopt を使うとライセンス的に問題ありそうな気がしますが大丈夫でしょうか?
C++予約語の問題:
C++としてコンパイルする場合に、ソース中の、new (class.cなど)および template (macro.hなど)という変数名が、C++の予約語であるため、問題が生じます。そこで、今回は、コンパイル・オプションで、この二つを -Dnew = new_ のように置換して凌いでいます(Cとしてコンパイルする場合には必要ありません)。
シンボリック・リンクの問題:
extライブラリのテスト時に、gosh xlink を用いて、シンボリック・リンクが適切な場所に作られます。今回の build では、gosh xlink がうまく動かないため、スクリプトを利用したコピーで凌ぎました。Windowsでは、シンボリック・リンクが、Unixほどは簡単に使えないのがいやな点です(対応するジャンクションという機構は存在する)。よく似たハード・リンクや、ショートカット(例: cygwin)で代用する方法はあります。
ソースの不具合と思われたところ:
・config.hに指定があるにもかかわらず、
#include <unistd.h> を #ifdef HAVE_UNISTD_H
で囲っていない。
・core.c において、
ScmObj Scm_VMFinalizerRun(ScmVM *vm)
の戻り値が、ScmObjなのに、何もreturnしていない。
・static ScmObj ref_val(ScmObj ref) の戻り値指定(ScmObj)がない。→暗黙で、intになっている?
・error.c
Scm_Error("make-compound-condition: given non-condition object: %S", SCM_CAR(cp));
未初期化の変数 cp にアクセスしている。
→ SCM_CAR(conditions)の間違い?
・core.cで、
#define DEFSTR(n, s) \
static SCM_DEFINE_STRING_CONST(n, s, sizeof(s)-1, sizeof(s)-1)
として、ロード・パス関連の文字列を、
DEFSTR(libdir, GAUCHE_LIB_DIR);
のように定義しているが、日本語を含むパスを渡した場合、sizeof(s)-1 != 文字数(s) となり、*load-path* がおかしくなる。
・.../ext/uvector/uvlib.stub.tmpl
return Scm_MakeString((char *)(SCM_UVECTOR_ELEMENTS(v)+start), ...
→return Scm_MakeString((char *)SCM_UVECTOR_ELEMENTS(v)+start, ...
SCM_UVECTOR_ELEMENTS(v)が void*型なので、ポインタ演算できない
・その他、不具合というわけではありませんが、C++としてコンパイルする場合には、型付けにうるさいようで、いくつかキャストを追加する必要がありました。たとえば、void * から、他のポインタ型への暗黙のキャストが許されないことが問題になりました(例:.../ext/uvector/uvlib.c.templ の、${SWAPB}((void*)d); で、(void*)がひっかかる)。
テスト失敗の原因追究
千代郎
次のバージョンに向けて、地道にやってます。
rfc.scm:
エラー内容
test mime-parse-message (7 - # binary body), expects ("multipart/mixed" 0 ("application/binary" 0 "\0\x01\x02\x03\x04\x05\x06\x07\x08\t\n\x0b\f\r \x0e\x0f\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19\x1a\0\r") ("application/binary" 1 "\r") ("application/binary" 2 "--") ("application/binary" 3 "--bb-")) ==> ERROR: GOT ("multipart/mixed" 0 ("application/binary" 0 "\0\x01\x02\x03\x04\x05\x06\x07\x08\t\n\x0b\f\r \x0e\x0f\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19"))
最初は、なかなか原因がわかりませんでした。分かってみれば簡単で、上記の出力を良く見ると、\x1a (=Ctrl-Z) で出力が切れています。この文字が EOF として認識されていました。これは、(C言語ストリームの)テキスト・モードでのファイル読み込み動作です。では、なぜ、テキスト・モードで読まれているのでしょうか? このテストでは、バイナリ・モードでの読み込みを期待しています。
GREPで調べた限り、Gauche中では、1つの例外(pipeの場合)を除いて、O_BINARY も、O_TEXT も、フラグを立てず、ただ単純に O_RDONLY としています。この場合、どちらのモードで開くのか、VC++の open() のリファレンスは言及していません (UNIXの場合は、O_BINARYになるのでしょうか?)。しかし、VC++に付属のCライブラリ・ソースを見ると、以下のようになっていることが分かりました。
(Microsoft CRTライブラリ open.c より引用)
if ((oflag & _O_BINARY) == 0) if (oflag & _O_TEXT) fileflags |= FTEXT; else if (_fmode != _O_BINARY) /* check default mode */ fileflags |= FTEXT;
となっており、O_BINARY も、O_TEXT も設定していない場合には、_fmode という変数に従うことになっています。問題は、この _fmode がどのように設定されているかです。
(Microsoft CRTライブラリ textmode.c より引用)
/*** *txtmode.c - set global text mode flag * * Copyright (c) 1989-2001, Microsoft Corporation. All rights reserved. * *Purpose: * Sets the global file mode to text. This is the default. * *******************************************************************************/ int _fmode = 0; /* set text mode */
ということで、デフォルトは、テキスト・モードということになっているようです(実際の挙動にも矛盾しません)。興味深いことに、デフォルトを変更したカスタム・ライブラリを作成するためのソースも存在しました。
(Microsoft CRTライブラリ binmode.c より引用)
/*** *binmode.c - set global file mode to binary * * Copyright (c) 1989-2001, Microsoft Corporation. All rights reserved. * *Purpose: * Sets the global file mode flag to binary. Linking with this file * sets all files to be opened in binary mode. * *******************************************************************************/ /* set default file mode */ int _fmode = _O_BINARY;
という訳で、ごちゃごちゃ書きましたが、要は、「Gaucheでは、open()のデフォルト読み込みモードをバイナリ・モードだと仮定しているようだが、VC++でのデフォルトはテキスト・モードである」というのが、テスト失敗の原因のようです。文化摩擦といった感じです。
以上の仮説を確かめるため、フラグがない場合に、O_BINARY を明示的に指定するように Gaucheに変更を加えたところ、rfc.scm テストを pass しました。
(実際のパッチでは、_fmode = O_BINARY; を含む vc-compat.c なりをリンクすることになると思います。一番単純なので。)
io.scm:
第1のエラー
test open-output-file :if-exists :append, expects cdefghij ==> ERROR: GOT #<error "unsupported file access mode 9 to open tmp2.o">
ファイルのオープンモード・フラグのうち、アクセス・モード(O_RDONLY/O_WRONLY のビット)を取り出すための、O_ACCMODEマスクがあります。この定義が、 VC++ にはないので、(VC++の)fcntl.hを見て、 O_ACCMODE = 0x000F と定義したのが誤りでした。これでは、O_APPEND のビットまで含んでしまい、上記のエラーになります。正しくは、O_ACCMODE = 0x0003 でした。完全に私のポカミスです。
第2のエラー
(sys-unlink "test.o")
で、test.oディレクトリが削除されず、別の場所でエラーを生じます。VC++には、一応 unlink() がありますが、UNIXとは異なり、ディレクトリの削除には対応していないことが原因でした(ライブラリのソースファイル(unlink.c)を見て分かりました)。Windowsでは、ファイルとディレクトリの扱いが区別されるのが習慣だからだと思います。そこで、ディレクトリの削除にも対応した unlink_() を自作し、解決しました。
ここまでの修正で、最後のテストの直前までパスします。しかし、最後のエラーは、まだ解決できず、困っています。最後の部分に、coding aware port 関連のテストが並んでいます。これらのテストのうち、末尾の3つを、(1)(2)(3)と呼ぶことにします。
現象は、(1)(2)までパスし、そこから処理が無限ループに入ったかのように、反応を返さなくなるというものです(Ctrl-C で中断する必要があります)。そして、(3)を取り除くと、テストは、pass で終了します。ここまでなら、(3)のテスト中になにかエラーが生じていると思いますが、
(1)(2)(2)(3)
と、(2)を重複させると、今度は、一度目の(2)の直後で反応が返らなくなります。さらに、
(1)(1)(2)(3)
と、(1)を重複させると、全部passしてしまいます。
ちなみに、上に書いた、rfc.scm での問題は、パッチをあてて解決されている状況下での話です。
このような、処理の順序に依存したエラーは、やっかいです。デバッガを使って、どこで処理がループに陥っているのか調べるために、デバッグ用のシンボル情報(.pdbファイル)をリンクしてビルドしました。すると、今度は、何ごともなかったかのように、全部passしてしまいました。うーむ。困った。しかし、デバッガによって、libgauche.dll中でループしていることは分かりました。
次の手として、printf作戦を展開し、coding aware port がらみの関数実行をトレースしてみると、
... Scm_MakeCodingAwarePort coding_filler coding_port_recognize_encoding look_for_encoding coding_filler coding_filler ok ←ここで、テスト(2)をパス coding_closer 無限ループ
となりました。なぜか、coding_closer()が10回ほど繰り返し呼ばれた後、coding_closer()中から呼んでいる、Scm_ClosePort()の PORT_SAFE_CALL 中でループするようです。なにか、ファイルのロックがらみで、変なことが起こっているのでしょうか(libgauche.dll, gc.lib ともに、シングル・スレッド用にビルドしているため、スレッドがらみの問題ではないはずです)。PORT_SAFE_CALLをトレースしようとして、その中にprintfを追加すると、また、症状がなくなってしまい、なかなか手ごわいです。ここでの疑問点は、
- なぜVC++以外では、問題になっていないのか。私がどこかでバグを入れた可能性もある。O_ACCMODEの例のように。
- なぜ coding aware port でのみ、問題が生じているのか?
2005/04/17 10:53:29 PDT追加:デバッガによって、port.hにあるマクロ、PORT_LOCK(p, vm)中の、以下の部分でループしていることをつきとめました。
while (p->lockOwner != NULL) { \ if (p->lockOwner->state == SCM_VM_TERMINATED) { \ break; \ } \ (void)SCM_INTERNAL_COND_WAIT(p->cv, p->mutex); \ } \
シングル・スレッド用のビルドの場合、SCM_INTERNAL_COND_WAITは、なにもしない空の関数なので、breakしない条件になると、抜け出せないのもうなずけます。問題は、なぜそのようになる場合があるか、です。
2005/04/21 06:05:48 PDT追加:ソースから理解したところによると、上記の場所は、Gauche VM によるポートのロックに関係します。複数のVMが存在する場合、1つのVMが、port->lockOwnerになると、他のVMは、(lockOnwerのVMが終了しない限り)そのポートをcloseできなくなります。上記の場所でループしているとうことは、あるVMがポートをロックしたまま、ずっと終了していないことを意味します。次に、ループ時の、p->lockOwnerのポイント先(= ScmVM構造体)をデバッガで見てやると、メモリ内容が、破壊されていました。例えば、stateの値は、enum宣言された変数であり、実際の値は0〜3のどれかになるはずですが、見当違いの値になっています。
つまり、このループ状態は、「実体を失っているVMが、lockOwnerに設定されており、他のVMが、stateがSCM_VM_TERMINATED(=3)になるのを永久に待っている」ということのようです。次の問題は、なぜ、内容の破壊されたVMが、lockOwnerに設定されているか?です。ソースを追った限り、lockOwnerの値は、NULLであるか、適切にアロケートされたVMのアドレスのどちらかになるように見えました。これ以上は、まだ分かりません。ループ状態が、微妙な環境の違いで生じたり生じなかったりすることも考え合わせると、ソースの静的な解析からは分からないような、なんらかのタイミングが関係した問題のように感じます。例えば、VM用にアロケートしたメモリが、GCによってなんらかの瞬間に解放されたりすることはあるんでしょうか?
- Shiro (2005/04/21 14:34:06 PDT): シングルスレッドでビルドしているんですよね?
この場合、作られるVMは一つだけで、それはsrc/vm.c中のstatic変数theVMに
格納されています。
- lockOwnerの指しているのはsrc/vm.cのtheVMの値と同じか?
- 同じだとしたら、src/vm.cのScm__InitVM中で唯一のVM実体をアロケートしているので、 そこに Scm_GCSentinel(theVM, "vm") を挿入してみる。ScmGCSentinelは GCされるはずのないオブジェクトに対して呼び出して、それがGCされるとwarningを 出してくれる。
- いずれにも該当しない場合は、別の原因でメモリが上書きされているってことですね。
2005/04/23 03:36:28 PDT追加:
あと少しのところまで来たと思います!
まず、アドバイスに従い、ひとつひとつ確認していきました。
シングルスレッドでビルドしているか? Yes
- gauche/pthread.hを消してもビルドできる。
- libgauche.dll, gosh.exe 中に、CreateThread() WinAPI のインポートがない。
lockOwnerの指しているのはsrc/vm.cのtheVMの値と同じか? No
- ループ時には、lockOwner = 0x009CED70 となっているが、Scm_VM() = theVM = 0x00965E58
Scm_GCSentinel(theVM, "vm") でwarningが出るか? No
これらの結果から、VMの実体(theVM)が破壊されたのではないと示唆されました。では、どこのメモリが上書きされたのか? port->lockOwner(=vm)が破壊されていないなら、次の可能性として、portの内容が上書きされている可能性があります。そこで、printf作戦によって、portの値をトレースすると、異常が検出できました
ループ状態に陥る直前の、Scm_ClosePort()に渡される port の値は、0x00C13678 です。しかし、トレース記録中で、この port への参照(iport)を持つ coding aware port を見つけると、作成時には、port の値は、0x00C159A0 なのです。全てのポート作成が一度は経由するはずの、make_port()での port のアドレスを全て見ても、0x00C13678 という port は存在しません。従って、port の値が、どこかで上書きされたことが分かりました。portのアドレスは、coding_port_data構造体に格納されており、この構造体のメモリーは、Scm_MakeCodingAwarePort()中で、SCM_NEWによってアロケートされています。
data = SCM_NEW(coding_port_data);
この、data のポイントするメモリ領域が、上書きされている可能性があるわけです。次に、この領域が、GCによって解放されているか、Scm_GCSentinel(data, "coding_port_data"); を追加して確認すると、確かに解放されています。ただし、Scm_GCSentinalを追加すると、ループ状態が再現されなくなってしまいました。
ちょっとやり方を変えて、
if(data == (void*)0xC136C0)DebugBreak();
として、上書きされているのでは予想した data (= 0x00C136C0) がアロケートされた直後に、デバッガをアタッチしました。このとき、port の値は、確かに、0x00C159A0 という、作成時の正しい値になっていました。この後のどこかで、この値が上書きされるはずです。そこで、data 指すメモリ領域に、メモリ・ブレーク・ポイントを作成し、書き込みがなされた瞬間に実行を停止するようにして、デバッガ上で実行を開始すると、見事に、port の値が、0x00C13678 に書き換えられる瞬間に停止しました。停止した瞬間のコンソール出力は、
... Scm_MakeCodingAwarePort coding_filler coding_port_recognize_encoding look_for_encoding coding_filler coding_filler
となっており、無限ループの始まる少し前に対応しています。このとき、libgauche.dll 中の 0x10069901 の命令を実行中でした。これは、関数 0x1006989E 内の命令であり、この関数は、コンパイラに出力させた map 情報とつきあわせると、
0001:0006889e _GC_reclaim_clear 1006989e f gc:reclaim.obj
に相当します。従って、まとめると、今回の症状は、「coding aware port の持つ、coding_port_data 構造体のメモリ領域が、coding aware port の存命中に、GC_reclaim_clear() によって上書きされるため、coding_closer()が無限ループに陥る」ということだと分かりました。GC_reclaim_clear()は、ソースのコメントによると、使用中のマークがついていないメモリ領域を、再利用する関数です。つまり、今回上書きされて困っているメモリ領域は、GCされたのだ、ということのようです。
- Shiro(2005/04/23 04:34:48 PDT): ファイナライザの呼ばれる順番が想定されているのと 違うのかもしれません。Scm_GCSentinelは 「回収されるはずのない」オブジェクトにファイナライザをつけるものです (元からあったファイナライザは上書きされます)。 ScmPortオブジェクトは独自にファイナライザをつけています (port.c, make_port())。 試しに、port_finalizeでファイナライズされているportが何かを表示してみると もう少し手がかりが得られるかもしれません。
2005/04/23 10:17:00 PDT追加: そろそろ、表題を「呪われたポートの謎」とでもしたくなってきました...(:-<
ファイナライザというのは、これまで知りませんでした。GCシステムから呼ばれるデストラクタ(C++の)のようなものですね。ポートのファイナライザを以下のようにしてみました。
static void port_finalize(ScmObj obj, void* data) { Scm_Warn("%S\n", obj); port_cleanup(SCM_PORT(obj)); }
これによって、ポートのGCをモニターすると、以下のように表示されました。
WARNING: #<iport (input string port) 00C15070> WARNING: #<iport (input string port) 00C15150> WARNING: #<iport (input string port) 00C36CB0> WARNING: #<iport (output string port) 00C15A10> ループ状態
最後に表示されているのが、問題の coding aware port であり、この port の GC 時に、ループ状態に入ってしまうことが(他のトレース情報も合わせ)分かりました。"(output string port)"という変な値になっているのは、port->name の指す先のオブジェクトも、上書きされたということなのでしょうか???(他の場所に、printfを加えたりして条件を変えると、crashするので、上の場合は、偶然 output string portのデータによって上書きされたと考えている)。
ここで、もう一度、問題点を整理してみます。 GCシステムからアロケートされた2つのメモリ領域が、参照の関係にあります。
coding aware port → (coding_port_data*)src.buf.data (1) (2)
(1)が(2)を参照する。
そして、今回の問題は、「(1)がGCされるとき、(2)がすでにGCされてしまっている。(1)のファイナライザで呼ばれる関数は、(2)の内容が有効であることを必要とするので、困ったことになる」と捉えています。
そもそも、(1)(2)が上記のような参照関係にあるとき、(2)が(1)よりも先にGCされることが、Boehm GC下で起こるものなのでしょうか?
Boehm GC doc/gcdescr.html, Introduction より意訳
マーク処理
変数から開始して、ポインタ参照の連鎖をたどり、到達可能なオブジェクトをマークする。
しばしば、コレクターは、ヒープ中のどこに本物のポインターがあるか知ることが出来ないので、
静的データ領域/スタック/レジスタの全てを、ポインタを含む可能性があるものとする。
コレクターによって扱われるヒープ・オブジェクトの中に、アドレスを表現するようなビット・
パターンが見つかると、それらは全てポインタとして処理される。コレクターに、
ヒープ・オブジェクトの配置に関する情報を明示的に知らせないかぎり、変数から到達できる
ヒープ・オブジェクトを、同様にスキャンし、ポインタを見つけることを繰り返す。
この通りであれば、(2)が(1)よりも先にGCされることはなさそうなのだが...。 (それとも、問題点の絞込み方を誤っているでしょうか? 今回は、アドバイスをあまり生かせませんでした。)
- Shiro(2005/04/23 17:30:36 PDT): うーむ、やっぱりGC絡みのようですね。
とりあえず、GCされる順番とファイナライザが走る順番とは別であることに注意して下さい
(1)が(2)を指している時、(1)が活きていれば(2)は決してGCされません。
(1)も(2)もごみになった時に、両者がGC候補になります。
上の引用は、ここまでの段階 (GC候補を見つける、mark段階) の動作ですね。
mark段階で見付けられたGC候補オブジェクトを回収するsweep段階の動作は次のように なります。
- GC候補のうち、ファイナライザがついているものがGC内部のキューに入れられる。
- ファイナライザ付きオブジェクトから指されているオブジェクトがmarkされる (ファイナライザ実行に必要なオブジェクトを回収してしまわないため)。
- 2.でmarkされなかったオブジェクトが回収される。
- ファイナライザが実行される。この実行は順不同で、(2)のファイナライザが (1)のそれより先に実行されることも有り得る。
- 一度ファイナライザを実行したら、該当オブジェクトからファイナライザが外される (ファイナライザが2回呼ばれることはない)
- ファイナライザがついているオブジェクトはこの段階では回収されませんが、 3.でポインタを活きているオブジェクトに保存するなどの動作が無ければ、次のGC サイクルで回収されます。
- 「ファイナライザの呼ばれる順序が不定」という問題には対応している つもりなのですが、漏れがある可能性は大いにあります。
- Gauche:Schemeコードのファイナライザも参照して下さい。
- あとGaucheRefj:gauche.vportの下の方の「Note on finalization」の説明も 参考になるかもしれません。
2005/04/24 02:07:54 PDT追加:
おつきあいいただき、感謝します。分かりやすい説明で、GCの実装イメージが理解できました。ご紹介いただいた「Note on finalization」の、1つめの項目の、X, Y が、まさに今回の(1)(2)に対応しますね。(2)がポートでなく、ただのデータであることを除けば。
これで分かったような気がしましたが、考えているうちに、疑問が出てきました。
- 今回の問題を、「ファイナライザの呼ばれる順序が不定」問題に分類してよいのか? (2)のオブジェクトには、ファイナライザが設定されていません。gc/finalize.c に、GC_dump_finalization()というものがあったので、これを、GC_finalize()の先頭に追加して、トレースしてみたところ、(1)のオブジェクトは、確かに "Finalizable object"として登録されていますが、(2)のオブジェクトは見つかりません。それとも、(2)に、概念的なfinalize過程(例えば、GC_reclaim_clear()とか)を想定しているのでしょうか?
- 説明の 2. で、ファイナライザ付きオブジェクト(1)から指されているオブジェクト(2)がマークされるなら、今回のようなことにはならないのでは?
- 今回の挙動が、GCシステムの仕様内の挙動であるなら、(1)のファイナライザで(2)を参照する安全な方法がないことになるのでは?
(冗談ですが、もし、ソフトがGCシステムを利用する飛行機があったとして、私は、乗りたくないなあ、と思ってしまいました。)
- Shiro(2005/04/24 02:24:37 PDT): (1)のファイナライザが呼ばれた時点で(2)のオブジェクトが「回収されてしまっている」 ことは無いはずです。あくまで不定なのはファイナライザが呼ばれる順番であって、 全てのファイナライザの実行が終わるまで、そこから指されているオブジェクトの 回収は待たれます。 (オブジェクトの「ファイナライズ」と「回収、再利用」は別のステージです。 「ファイナライズ」の段階でGCシステムが勝手にオブジェクトの中身を書き換えることは ありません。書き換えてたらそれはGCシステムのバグです)。
- 但し、ひとつ見るべきところがあります。この振舞いは、GC_JAVA_FINALIZATIONが 定義されている時に有効になります。Boehm GCは用途によって ファイナライザの振舞いを何通りかにカスタマイズできるのですが、Gaucheのような 使いかたをする場合はこのシンボルの定義が必要で、定義されてないと (1)のファイナライズ段階で(2)が回収されてしまっていることが起こり得ます。 ビルド環境でこのcppシンボルが定義されているかどうかを確認してみて下さい。
- GCシステムは必要な信頼性という点ではOSと同じようなものでしょう。 ただ、プログラムによって最適なGC戦略というのが大きく異なるため、 決め打ちで一つに出来ないのが難しいところです。(なお、「まだ参照され得る オブジェクトを回収してしまう」というのは明らかなGCシステムのバグなんですが、 GCアルゴリズムによっては「ごみなのに回収されないことがあり得る」、という ものもあります。conservative GCと呼ばれるもので、Boehm GCはそのタイプです。 ミッションクリティカルなシステムでは、ごみが確実に回収できることを保証する アルゴリズムが使われます)。
2005/04/24 04:40:52 PDT追加:
GC_JAVA_FINALIZATION (正しくは、JAVA_FINALIZATION ですね?)は、コンパイル時に定義していません(gc/NT_MAKEFILEというBoehm GC 付属の既定のMakefileを使っています)。定義してビルドすると、gc.lib以外は、同じ条件で、io.scm テストをパスするようになりました! まだ、油断はできないかもしれませんが、とりあえず、祝杯を上げております。今回は、いままであまり考えたことのないことに出会い、大変勉強になりました。ありがとうございます。
- Shiro(2005/04/24 11:49:14 PDT): ああ、NT_MAKEFILEを使ったのですか。
だとしたら他にもコンパイルフラグで問題が出る可能性があります。
gcのコンパイル時に以下のフラグが設定されていることを確認して下さい。
- NO_SIGNALS
- NO_EXECUTE_PERMISSION
- ALL_INTERIOR_POINTERS
- DONT_ADD_BYTE_AT_END
- JAVA_FINALIZATION
- GC_GCJ_SUPPORT
- ATOMIC_UNCOLLECTABLE
コメント、議論
emeitch(2005/04/10 20:01:42 PDT):移植お疲れ様です!さっそくコンパイルさせていただきました。
とりあえずこちらでのビルド環境での結果を報告させていただきます。(Windows2000 sp4、VC++ 7.1、WSH ver. 5.6)
初回、 readme_1st.txt に従い、make.bat まで行いました。その途中、Makefile.inで言うと、79行目 del gosh$(EXEEXT)で、削除対象ファイルが無いとのエラーで、delコマンドからエラーのリターンコードが返され、makeが途中で終了しました。とりあえず、srcフォルダに、ダミーのgosh.exeを突っ込んで、また0からmakeしたら通るようになりました。その後、gosh起動までは確認いたしました。とりいそぎ、ご報告まで。
- 千代郎 ビルド成功のお知らせ、ありがとうございます。なんか入れ忘れとらんやろか、と心配していたので、ほっとしております。del に失敗する件は、うちの環境(WindowsXP, VC++ 7.0/7.1)では、問題ないようです。エラーが出ても、nmakeは継続されました。ただし、makeを cygwinの GNU make でやると、emeitchさんと同じ症状になったので、makeの仕様の差かもしれません。いずれにせよ、Makefileを書き直す必要がありそうです。
- emeitch(2005/04/17 18:37:46 PDT):一応、cygwin環境をPATHから外して、また0からビルドをかけてみましたが、同じ結果でした。エラー出力も、“NMAKE : fatal error U1077: 'del' : リターンコード '0x1'”との内容なので、nmakeからのものと思われます。ちなみに、nmakeのバージョンは7.10.3077です。
- 千代郎(2005/04/17 20:07:02 PDT):わざわざお知らせ、ありがごうございます。http://www.microsoft.com/JAPAN/developer/library/vcug/_asug_nmake_options.htm を読むと、emeitchさんの言ってらっしゃる nmake の挙動が正しい仕様(=コマンドが0以外の終了コードを返した場合、中断される)のようです。私の手元の挙動は、なぜか、ちょうど /K オプションをつけたときのようになっているみたいです。nmakeのバージョンは、7.10.3077で全く同じで、いったいなぜこうなるのか、不明です。初回にdelが失敗する部分は、今回のMakefileの他の部分(testなど)にもあるので、他にもご迷惑をかけた方がいるかもしれません。次のバージョンでは、直します。それにしても、自分の手元の結果だけ見ていると、怖いですね。上で苦しんでいる io.scm の件も、私の別のパソコン(OSも、VC++のバージョンも異なる)では、症状が出なかったりするんです。不思議です。もちろん、全てに合理的な説明があるはずですが。
- emeitch(2005/04/17 22:27:43 PDT):確かに謎の挙動ですね。 千代郎さんのビルド環境で、ここに示されているパスに、tools.iniが入り込んでる可能性はないでしょうか?とりあえず、こちらで考えられる原因はこれぐらいです。将来的には、(Windowsなので)バイナリパッケージでの配布になるでしょうから、この問題は瑣末でしたね。無駄に引き伸ばしてすみません...。
千代郎(2005/04/25 03:21:15 PDT):そろそろ、これまでの改訂をもとに、Gauche-0.8.3w-02 にまとめたいと思います。-01に関する内容を、別ページに移動させました。
- ところで、一つ、詳しい方にご相談したいことがあります。ext/dbm を移植し、dbm.gdbm を利用できるようにしました。平林幹雄さんという方の作成されている、QDBM(http://qdbm.sourceforge.net/ )を使っています。
そこで、次回の配布に、qdbm.dllを添付したいと思っています。QDBMは、GPLでライセンスされています。また、他にも、前回の配布の段階で、すでに、GNU getopt を使用しています。この場合、GPLで保護されたものが、混ざってくるわけですが、Gaucheのもともとのライセンスは、BSDライセンスです。ここで、
- 全体のライセンスは、より縛りのきついGPLになるということでよいのか?
- shelarcy : 両ライブラリとも LGPL で、よりきつい GPL にするということですよね? 念のため。
- 千代郎: ご指摘ありがとうございます。LGPLというものを知りませんでした。http://ja.wikipedia.org/wiki/LGPL で調べて、理解しました。説明によっては、http://e-words.jp/w/LGPL.html 「ダイナミック・リンクに限り」という制限があるので、原文を調べたところ、項目5で、ライブラリを使用することと、ライブラリを含むことの違いが分かりました。ということは、qdbm.dllに関しては、Gaucheと別のアーカイブにして(こちらはLGPL)、一緒にダウンロードしてもらうようにすれば、ライセンスのことは変更の必要がないということですね。気が楽になりました。
- もしそうだとして、皆さん、どう感じますか?(こういう場合にどういう選択をすべきか、コンセンサスはあるんでしょうか? 例えば、「〜を目指しているのなら、〜したほうがよい」、などなど。)
- 全体のライセンスは、より縛りのきついGPLになるということでよいのか?
- shelarcy (2005/04/25 06:36:44 PDT): sourceforge.jp の方に Gauche-qdbm があるので qdbm は別のモジュールにしてしまった方がいいのではないでしょうか? ……もちろん、VC++ 版のためにこの際 qdbm のモジュールをパッケージに含めてしまうという選択肢もありだと思いますが。
- ところで、Gdbm の Windows 版バイナリは使えませんでしたか? Gauche:Windows/MinGW で挙げておいた方のライブラリは古いせいか MinGW 版でうまく使えませんでしたが、こちらはどうでしょうか?
- 千代郎(2005/04/26 02:15:24 PDT):探してみたら、BSDライセンスのgetoptもあるんですね(http://freegetopt.sourceforge.net/ )。QDBMは、GDBM互換APIを利用しているだけです。なぜ、QDBMを選択したのかは、ごく些細な理由からです(GDBMのバイナリ版については知りませんでした。ご紹介ありがとうございます)。
- GDBM互換APIを提供している。
- アーカイブのサイズが小さく、コンパクトにまとまっている印象だった。
- 付属のMakefileで、VC++ですぐにビルドできた。
- 私のVC++版の目的は、VC++という身近なツールで開発・実験ができるようにすれば、Windows派の人が気軽に参加でき、Windows用のおもしろい拡張やフロント・エンドが出て、盛り上がらないかな、ということです。正直言うと、ライセンスに関しては、何のポリシーもありません。ただ、そういう姿勢でいいのかな、と聞いてみました。