Gauche:Windows/VC++

Gauche:Windows/VC++

VC++用ポートについて。

過去: Gauche:Windows/VC++:log:detail_2005 Gauche:Windows/VC++:log:detail_2005_2 Gauche:Windows/VC++:log:old_2003 Gauche:Windows/VC++:log:old_2004 Gauche:Windows/VC++:log:old_2005 Gauche:Windows/VC++:log:old_2007


Shiro (2012/11/26 00:50:40 UTC): 現在、WindowsネイティブサポートはMinGWに一本化しています (Gauche:Windows/MinGW参照)。VC++のプロジェクトファイル等はソースツリーからは 削除 (Commit 43eb70774196 ) しましたが、リポジトリには残っているのでresurrectしたい方はどうぞ。


Shiro (2008/04/17 03:30:14 PDT): 過去分ログに移しました。 ここを参照する人もいるので現在の状況だけメモっときます。

詳細はソースツリー中のwinnt/README.txtを参照してください。


cielacanth:VCでのビルドを試みた者です。私にも上手くできましたし、テストも通るべきものは通ったと思います。素晴らしいです。

ただ、途中ちょこちょこ変更が必要だったのでそれを書きます。変更点には

    1. 自分の環境でコンパイルするためにmustな変更
    2. VCやwindowsという状況を鑑みたベターな変更

をあわせて書いておきました。適時取り捨て選択して頂けると幸いです。

(パッチは注意して作成しましたが、VC/cygwin/mingwと同時並行的にコンパイルしているためどうしても抜けが出てしまいます。もしCVSにコミットされた場合、パッチに間違いがあれば再度パッチを送ります)

 @@ -216,16 +216,17 @@
   (define (ensure-node container ref set key)
     (or (ref container key #f)
 -        (rlet1 v (make-vector 64 #f)
 -          (set container key v))))
 +        (let1 v (make-vector 64 #f)
 +          (set container key v)
 +        v)))

 これでとりあえず動くようにはなりましたが、この変更が正しいかどうかは分かりません。

 // header.h
 #ifdef BUILD_DLL
   #define DLL_API extern __declspec(dllexport)
 #else
   #define DLL_API extern __declspec(dllimport)
 #endif
 DLL_API void DLL_dummy();
 // implA.c
 #define BUILD_DLL
 #include "header.h"
 // implB.c
 #include "header.h"
 void DLL_dummy() {}
 typedef struct WINDATAA_ {
   LPCSTR str; // LPCSTR = const char *
 } WINDATAA;
 WINAPI void WinFuncA(WINDATAA *data);
 
 typedef struct WINDATAW_ {
   LPCWSTR str; // LPCWSTR = const wchar_t *
 } WINDATAW;
 WINAPI void WinFuncW(WINDATAW *data);
 
 #if defined(_UNICODE)
  #define WINDATA WINDATAW
  #define WinFunc WinFuncW
 #else
  #define WINDATA WINDATAA
  #define WinFunc WinFuncA
 #endif

以上の修正を加えたパッチをここにおいて置きます。 http://sund1.sakura.ne.jp/uploader/source/up22354.zip


cielacanth: まずdllのエクスポートシステムについて若干の説明をします。すでに知っていたらごめんなさい

windowsで関数や変数をエクスポートするための方法

VCでDLLからエクスポートする際はこのようなルールが適用されます。

 // 変数(正確には パブリックデータシンボル or オブジェクト)
 shared library 作成時
   extern __declspec(dllexport)
 shared library 使用時
   extern __declspec(dllimport)
 static library 作成時/使用時
   extern
 // 関数 (変数と同じルールでもかまわない)
 shared library 作成時
   extern __declspec(dllexport)
 shared library 使用時
   extern /* ここが違う */
 static library 作成時/使用時
   extern

関数の場合は、インポート時に__declspec(dllimport)を明示する必要がありません。これはstatic libraryを利用するときは非常に便利だったりします。

また、これらは実体が定義されたとき、そいつが参照しているプロトタイプの定義が使われるようです。つまり、export定義のtestfunc関数プロトタイプを作り、全く関係ないところでtestfuncを実装しても関数はエクスポートされません。また、extern export定義とextern定義のみのプロトタイプで関数が二重定義された場合、プロトタイプの順番によってエラーになったりならなかったりします。どうも面倒なことに足を突っ込みたくなければ、同一プロジェクト中ではできる限り同じ定義を使うのが望ましいようです。

参考: http://msdn2.microsoft.com/ja-jp/library/kh1zw7z7(VS.80).aspx

となります。これだけでも十分おなかいっぱいになれそうなのですが、静的リンクの場合も併せると事態はさらに面白いことになります。特に変数を直で使う場合には、使う側がSCM_EXTERN=importを明示しないといけないのが痛いところです。

現在のクラスエクスポート方式について

ここではScm_StringClassを例に扱っています。

最初この理由がわからなかったのですが、その原理はこのようになっているようです。

  1. (おそらくwindowsの)仕様で、DLLからインポートされたシンボルは常にその先頭に'_imp__'の文字がつく。
  2. DLLからインポートされたパブリックデータは、その実アドレスがコンパイル時に決まらないなどの理由で、普通はポインタとして扱われる。
  3. つまり、DLLからインポートされるオブジェクトType objは普通Type *_imp__obj(= &obj)として扱われる。
  4. これはgaucheが内部でやっていることと全く同じである。
  5. つまりlibext.dllが自動的にScmClass *_imp__Scm_Stringの実体を作成するため、これを宣言しただけでリンクエラーが出ることも、エラーが出ることもなく使用できた。

最初このハックを知ったとき驚いてしまいました。今でも、よく考えついたものだと感心しています。

このやり方が非常に上手いということは認めます。それは認めるのですが、以下の3つの理由からこれはやめた方がいいと思います。

理由は正直良く分からないのですが、リリースモードでは最初からdllのベースアドレスを調整しているのかもしれません。 こういうことを自動的にやってくれるツールもあります(これは違うかもしれません。う~む)。

 コラム(?)
 windowsでdllを読み込むときにはこのような動作になっています。
 「dllにはデフォルトベースアドレスというものが存在し、
   dllの読み込まれたアドレスがこれと違った場合には必要な個所をすべて書き換える」
 
 具体的にはこうです。
 1. 最初dllを読み込むときは、とりあえずそのdllに設定されたベースアドレス
    (大体デフォルト値=0x10000000)が空いていると仮定して読み込む
 2. 失敗したら他のアドレスを試し、成功した暁には必要なアドレスをすべて書き換える
 3. rebase.exeというツールを使えば、最初からdllのベースアドレスを調整し
    再配置セクションごと消し去ることが可能

何はともあれ、とりあえずこのやり方はVC8のリリースモードには通用しません。表に現れる不具合としては、_imp__Scm_StringClass に二つのサイズがあるという警告と、オブジェクトが参照されている場合にはリンクエラーが出ます。これは自前で宣言したScmClass*型の_imp__Scm_StringClassオブジェクトと、dllからエクスポートされたScmClass型オブジェクトのどちらの型が正しいのかリンカには判断できないためです。

VC8のリリースモードに限って言えば、最適化オプションでどうにかなるような気がしなくもなくもなくもなくもないのですが、自分にはよく分かりません。

解決方法

方法はいくつか考えられますが、そのうちの正攻法のいくつかをここに書いておきます。まあ、いくつかとは言ってもそんなに大した数では無いのですが(^^)

その1: すべてのモジュールで'_imp__Scm_StringClass'の実体を定義する

これは例えば、_imp__Scm_StringClass_scmimp__Scm_StringClass などの名前に変更し、すべてのモジュールで_scmimp__Scm_StringClassの実体を定義するということです。

簡単っちゃあ簡単です。

その2: staticなオブジェクトを使うこと自体をやめる

静的オブジェクトを使わず、初期化関数ですべてのことを行うようにします。 型の判断にもオブジェクトは使用しないようにします。 特にこれだと、ライブラリ使用時にその種類(staticかsharedか)を区別する必要がなくなるので、 とても楽なのです。

その3: コード全体をC++でコンパイルする。

こうするとScm_StringClassのアドレスが定数として扱えるようになります。またGAUCHE_BROKEN_LINKER_WORKAROUNDの定義自体も不要になります。

当然、このやり方を行うにはプロジェクト全体をC++言語としてコンパイルする必要があります。実際にやってみた感じでは多少の変更が必要になりますが、まあどうにかなるレベルです。この辺りは千代郎さんのやったこととほとんど同じような感じですね。

過去ログから: Gauche:Windows/VC++:log:old_2005 (Gaucheを、VC++に移植するに当たって判明したこと)

これに追加して自分の環境では

が必要でした。また、

ということに気をつける必要があります。 (「現在の言語はCかC++か?」「C++ならextern "C"は定義されているか」「ヘッダをインクルードしたときのSCM_EXTERNの定義は何か?」「ソースのSCM_EXTERN定義は何か?」「_UNICODEは定義されているか?」などを適切に判断していけばOKです)

具体的には?

 // libext.h
 #include <gauche/class.h>
 #include "gauche/ext2_header.h" // 他の拡張ライブラリのヘッダはここで定義する必要があります
 
 // 他の拡張ライブラリから呼ばれた場合に備えて
 // #undef LIBGAUCHE_EXT_BODY // この方が安全かも
 #if LIBEXT_EXPORTS
     #define LIBGAUCHE_EXT_BODY
 #endif
 #include <gauche/extern.h>
 
 SCM_EXTERN void Scm_Xxx();
 
 // 他の拡張ライブラリから呼ばれた場合に備えて
 #if LIBEXT_EXPORTS
     #undef LIBGAUCHE_EXT_BODY
 #endif
 // libext.c
 // LIBGAUCHE_EXT_BODY は定義しない
 #include <gauche.h>
 #include <gauche/class.h>
 #include "libext.h" // 普通に呼び出せばLIBGAUCHE_EXT_BODY は定義される
 
 // libext.hはこの定義を消してしまうので、インクルードファイルリストの最後に再度定義
 #define LIBGAUCHE_EXT_BODY
 #include <gauche/extern.h>
 
 // implementation...
 

これをすべての拡張ライブラリが守れば、他の拡張ライブラリのヘッダを読んだとしても、 LIBGAUCHE_EXT_BODYが二重定義されたり定義されなかったりすることはなくなります。 まあ、とってもメンドクサそうっていのは直感的に分かりますし、不具合が起こったときの対処が非常に難しくなる可能性もあります。

これは比較的多くのライブラリで採用されている方法なのですが、具体的にはこのようにします。

 // この拡張ライブラリの基本ヘッダ
 // libext.h
 #ifdef LIBEXT_EXPORTS
     #define SCM_EXT_EXTERN SCM_EXPORT // SCM_EXPORT = __declspec(dllexport)
 #else
     #define SCM_EXT_EXTERN SCM_IMPORT // SCM_IMPORT = __declspec(dllimport)
 #endif
 
 #include <gauche/class.h>
 #include "gauche/ext2.h" // ヘッダはどこにおいても構わない
 
 SCM_DECL_CLASS_EXT(SCM_EXT_EXTERN, Scm_ExtClass) // 新しく定義しましたが、内容はわかると思います。
 
 SCM_EXT_EXTERN void Scm_Xxx();
 // libext.c
 #include <gauche.h>
 #include <gauche/class.h>
 #include "libext.h"
 
 // implementation with SCM_EXT_EXTERN...
 

問題はSCM_DECL_CLASS(_EXT)に、gcc系では不要なEXPORT定義が必須になってしまうことです。

議論

__imp_について

Shiro(2008/04/24 07:42:55 PDT): やあ、詳細な分析ありがとうございます。 ご指摘のとおり、gaucheが__imp_を使ってるのはVCがDLLのimport/export時に やっていることをエミュレートしているものです。 しかし、こんなことをしなくてもリンカががんばれば外部DLLに定義されている データをコンパイル時定数として扱うことはできるはずで(現にgccではやっている)、 将来のVCでこのハックが必要無くなる可能性も高いでしょう。

それがあるべき姿のはずで、今のハックはarcaneなVCの仕様に嫌々 付き合っているようなものなので、不要になればそれに越したことはないです。 そういう意味ではあくまでtransientなハックなので、 動きさえすればdirtyであっても構わないと考えています。 この観点からは、当面解決すべきはreleaseビルドを何とか通すことですね。

私のアジェンダとしては、

ってとこですね。ところでReleaseビルドにすると、外部DLLで定義された Scm_FooClassのアドレスがコンパイル時定数扱いになるんでしょうか。 そうだとしたら__impハックをDebugビルド限定で使うってのはありかもしれません。 でなかったら__scmimpみたいに自前で間接ポインタを用意する方向ですかね。

Shiro(2008/04/25 04:19:59 PDT): ああ、それならC++でコンパイルするのでいいと思います。 gcについても、7.x系列はANSI宣言になっているはずなので。7.1が正式に出たら 置き換えようと思っています。

EXTERNマクロについて

どちらの方法を取るにしても、私が問題だと思うのは、

"Programs must be written for people to read, and only incidentally for machines to execute."の 精神に立ち返るなら、こういう非本質的なコードは人間が見えるところに 置くのではなく機械に生成させるべきです。なのでいっそのこと、 VC用にソースをプリプロセスする際にVC専用のヘッダファイルを生成する方が すっきりするような気がします。

 // gauche/ext_extern.h
 #include <gauche/extern.h>
 #undef LIBGAUCHE_EXT_BODY
 // libext.h
 #include <gauche/class.h>
 #include "gauche/ext2_header.h"
 
 #if LIBEXT_EXPORTS
     #define LIBGAUCHE_EXT_BODY
 #endif
 #include <gauche/ext_extern.h> // ファイル名は適当
 
 SCM_EXTERN void Scm_Xxx();
 // libext.c
 #include <gauche.h>
 #include <gauche/class.h>
 #include "libext.h"
 
 #define LIBGAUCHE_EXT_BODY
 #include <gauche/ext_extern.h>
 
 // implementation...
 

cielacanth(2008/05/02 04:01:03 PDT): 以前言ったパッチを作成いたしました。内容に重複がありますが改めて書きます。主な変更点は以下のとおりです。

    1. http://d.hatena.ne.jp/softether/20041201 :win9x系では*W系の関数がことごとく使えないそうなので、この辺全部置き換えた方が安心かもしれません。
    2. windowsではリリースモードでコンパイルしたdllに'xxx.dll'、デバッグモードでコンパイルしたdllに'xxxd.dll'や'xxx_d.dll'などの名前を付けることが一般的によく行われているためです。これがエラーになるのは少し困るのです。
    3. 拡張ライブラリの関数/変数のエクスポートのやり方がまだ決まっていないので、この辺り適当にお茶を濁しています^^

http://sund1.sakura.ne.jp/uploader/source/up22838.zip (パッチのファイル名は'dir_dir2_filename_ext.patch'となっています)

Shiro(2008/05/02 05:04:01 PDT): ありがとうございます。ひとつひとつ私自身で咀嚼してから パッチを当てて行くのでしばらく時間をいただくと思いますが、徐々にマージします。

win9x系のサポートは、正直荷が重いと思っているのですが (NT移行にしかない Win APIもいくつか使っていると思います)、どのくらい必要なものでしょうかねぇ。 世の中にまだ稼働中のwin9xマシンがあることは承知しています。 「そういうマシンでwin native gaucheを走らせたい」という要望が、 開発とメンテの負担の増加に見合うだけあるのかどうかというのが問題です。

cielacanth(2008/05/03 02:54:29 PDT): 調べてみると、win9x系のシェアはOS全体の2%程度でした。必要性はたしかに微妙かもしれません・・・ とりあえず今は、ほかの変更のついでにソースが簡単になることが分かった場合のみ、適時変更していくことにします。

sakiyama(2008/05/13 22:24:58 PDT) リリースモードですが、プログラム全体の最適化(/GL),リンク時のコード生成を使用(/ltcg)を切ると通りました。個人的にですが、C++としてコンパイルは不必要な作業(VC++のみで)が増えるので、使わない方がいいかなあと思います。ANSI系のAPIも、Unicode文字セット前提にして、非サポートでいいんじゃないでしょうか。それとprocess.hはGC.hより先にインクルードしないとダメでした。

cielacanth(2008/06/02 15:54:40 PDT): 最近ネットにつなげない環境にいるので返信が遅れてしまいました。すいません。 もちろん、C++でコンパイルしなくてもいいのですが、その場合には 「Gaucheをコンパイルする場合、VC6ならxxxのコンパイルオプションを指定してyyyを指定しないでください。VC7なら… VC8なら…」 という説明を延々と書く必要が出てきます。また、それを確認する作業も必要になります。別にそれでも構わないとは思うのですが、なんというか、VCに恨みでもあるのかな? みたいな。まあ冗談ですけど^^ またsakiyamaさんのやり方だと、意図的にプログラムの最適化を抑えることになりますよね。関数呼び出し時の間接ジャンプが無くなること自体は別に悪いことではないですし、gaucheをコンパイルするために「プログラム全体の最適化」や「リンク時のコード生成を使用」オプションを切ってしまうと、ほかの最適化機能も必然的に抑制せざるをえなくなります。これがどの程度速度に影響するかはちょっとわからないのですが、全体にあまりいい影響を与えない事は容易に想像がつきます。 文字コードに関しては、ソースが簡単になる場合のみ適時変更するようにしています。

> それとprocess.hはGC.hより先にインクルードしないとダメでした。 VCはVCでもバージョンによって微妙に違ったりするので、そのあたりが関係あるのかなぁ。 少なくとも自分の環境では再現しませんでした。ちょっと分かりません。すいません。

 C:\Program Files\Microsoft Visual Studio 8\VC\include\process.h(54) : error C2059: 構文エラー : '{'
 C:\Program Files\Microsoft Visual Studio 8\VC\include\process.h(54) : error C2059: 構文エラー : '型'
 C:\Program Files\Microsoft Visual Studio 8\VC\include\process.h(59) : warning C4273: 'GC_beginthreadex' : dll リンクが一貫していません。
        c:\work\Project\gauche\Gauche-vs2005\gc\include\gc.h(1049) : 'GC_beginthreadex' の前の定義を確認してください
 C:\Program Files\Microsoft Visual Studio 8\VC\include\process.h(60) : warning C4273: 'GC_endthreadex' : dll リンクが一貫していません。
        c:\work\Project\gauche\Gauche-vs2005\gc\include\gc.h(1054) : 'GC_endthreadex' の前の定義を確認してください
More ...