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): 過去分ログに移しました。
ここを参照する人もいるので現在の状況だけメモっときます。
- VC++でのコンパイルは ext/ 以下も含めて可能。ただしDebugビルドでしか確認してない
- CVSやtarballからビルドする場合は一度プリプロセスする必要がある。
プリプロセスにはcygwin版goshが必要。
正式リリース時にはプリプロセス済みソースを配布予定。
- gauche.netとか、コンソールでの日本語入出力などに未対応部分がある。
- インストールプロジェクトをまだ作ってないので、インストールできません。
試すには、winnt/Debugに降りてgosh -ftestなどとしてください。
詳細はソースツリー中のwinnt/README.txtを参照してください。
cielacanth:VCでのビルドを試みた者です。私にも上手くできましたし、テストも通るべきものは通ったと思います。素晴らしいです。
ただ、途中ちょこちょこ変更が必要だったのでそれを書きます。変更点には
- 自分の環境でコンパイルするためにmustな変更
- VCやwindowsという状況を鑑みたベターな変更
をあわせて書いておきました。適時取り捨て選択して頂けると幸いです。
(パッチは注意して作成しましたが、VC/cygwin/mingwと同時並行的にコンパイルしているためどうしても抜けが出てしまいます。もしCVSにコミットされた場合、パッチに間違いがあれば再度パッチを送ります)
- コンフィギュアファイルの改行文字(must): cygwinのインストール時にデフォルトの改行モードを選べる(うちではLFを選択)のですが、コンフィギュアファイルの改行モード(CRLF)がそれと違ったため、ことあるごとにそれで詰まりました。CRLF改行モードを正しく扱ってくれないツール自体がちょっとあれっちゃあれなんですが。これは、コンフィギュア関連ファイルの改行文字を手当たり次第に置き換えることで対応しました。
- Shiro(2008/04/21 06:56:58 PDT): プリプロセス時に生成されるファイルがLF改行になってしまって
VCでちゃんと読んでくれない、ということかな?
- cielacanth: コンフィギュア関連ファイル(./DISTやconfigure.ac, config.subなど)の文字コードがCRLFなので、自分のところのcygwinで上手く読めませんでした。設定でどうにかなるのかもしれないのですが、自分はよく知らないのです。
- Shiro(2008/04/21 08:07:58 PDT): あれ、そのへんのファイルはLF終端になってますよ。
もしかしてCVSのチェックアウト時に変換がかかってしまったりしてませんか?
(でもcygwinはCRLFでも読んでくれると思ったけどなあ?)
- cielacanth: そうだったのですか。変換がかかっている可能性は十分にありそうなので、ちょっとこちらの環境を見直して見ます。
- cielacanth: そのとおりでした。別途cygwinにcvsをインストールしたら正しく動作するようになりました!
- gauche/charconv/cvt.scmが失敗する(must): gauche-0.8.13(cygwin)のgoshでは、このファイルの実行に失敗します。
@@ -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)))
これでとりあえず動くようにはなりましたが、この変更が正しいかどうかは分かりません。
- Shiro(2008/04/21 06:56:58 PDT): あ、そうか。rlet1は0.8.13リリース後に導入したんだった。
./DIST winntで失敗するってことですよね? cygwin版の方もCVS HEADを入れておいたら
通りませんか。
- cielacanth: CVS HEADなら通ると思います。ただ、CVS HEADからgoshを生成するためには過去のgoshが必要です。
- Shiro(2008/04/21 08:07:58 PDT): つまり、(1)cygwin版0.8.13をビルド・インストール、
(2)それでもってcygwin版CVS HEADをビルド・インストール、
(3)それでもって./DIST winnt、ということです。
まあ激しく面倒ですね。なるべくはやくWinVC用ソースを配るようにします。
- cielacanth: ありがとうございます。
- GC_DLLの定義をプロジェクトファイルに追加(must / GC関連関数をエクスポートしない場合はmust not): windowsでは実体を定義するファイルがインクルードしたヘッダの定義がエクスポート定義に使われることがあるようです。つまり
// 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() {}
- こういうことをやってしまうと、dllファイルにはエクスポートされてもlibファイルにはエクスポートされないの事態に陥ってしまうことがあるようです。実際、cvsから得たgaucheをコンパイルすると、gosh作成時に GC_get_XXX_size などのリンクに失敗します。これはGC_DLLがgauche.hの先頭で定義されているため、gc/xxx.cがgc.hをインクルードした場合はGC_DLLが定義されないからです。そのため、libgaucheプロジェクトファイルにGC_DLLの定義を追加しました。
- (u)intNN_t の定義を止めた(better): config.hでint32_tなどの定義が行われています。このようにする理由は分かるのですが、たまに同じことをしているライブラリを見かけます。そういうライブラリと共存させようとすると型名が衝突してどうしようもない状態になってしまうので、ことVCに関してはこれやめた方がいいと思います。これを行うとuint16_tが定義されていないのでext-netがコンパイルできなくなりますが、ちょっとした手間でフィックスできます。
- Shiro(2008/04/21 06:56:58 PDT): config.hで定義してるのはどっちかというと
stdint.hが無いVC2005のための緊急回避措置です。そもそもC99で定義されてる型名
だから自前でtypedefしちゃうのが良くないのは当然なんですが…
VC2005のためだけにintNN_tに関してコードの他の部分をいじるのはあまり気が
進まないなあ。VC2008だとどうなんだろう。
- cielacanth: この所為でSDLという有名ライブラリと共存できないので、採用して頂けるととても嬉しいです。まあお気持ちはよく分かるので、よかったらってことでお願いします。VC2008についてはちょっと分かりません。すいません。
- Shiro(2008/04/21 08:07:58 PDT): 了解。SDLと共存できないのは痛いですね。
- _UNICODEやTCHARの使用についての説明: Windowsの文字コードの扱いについて若干の説明を行っています。ここが分からないと、後の変更理由がわからないと思うので一応書いたのですが、すでに知っていたらごめんなさい。
- windowsでは_UNICODE or UNICODEの定義によって呼び出されるAPIが変わり、それに伴い使われる構造体の定義も変わります。典型的にはこのようなことをしています。
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
- このようなハックを行う理由は、Unicodeを使うプログラムとマルチバイトを使うプログラムの共存と、文字コードの一貫性を保証するためです。このおかげでwindows内部では使われる文字コードを常にUTF16に固定することができます。ただ_UNICODEが定義されているかいないかで呼び出される関数が変わるので、場合によっては面倒なこともあります。
- Scm_MBS2WCS / Scm_WCS2MBS関数が常に必要(must):
- system.c の convert_user関数ではUSER_INFO_2構造体を使っていますが、これは_UNICODEの定義に関係なく常にUTF16用の関数しか提供されていません(AやWの区別が無い)
- passwd構造体は常にマルチバイト文字列を使っています
- という理由により両者の文字列には常に互換性がないのですが、もとのソースでは_UNICODEが定義されている場合しか必要な変換を行わないようです。そこでその変換を常に行うために、SCM_WCS2MBSではなくScm_WCS2MBSを使うようにしました。また、Scm_WCS2MBSが_UNICODEが定義されている場合しか定義されないようになっていたので、それも合わせて変更しました。
- wcs2mbs/mbs2wcsの定義の変更(better?)と、mallocで文字列データを確保しないようにしました(better/must): もとのソースでは変換後の文字列を格納するメモリ領域をmallocで確保する場合があり、メモリリークの可能性があったのでそれを消しました。副作用としてconfig-gauche.cがコンパイルできなくなってしまったのですが、コンパイルを通すためにこれらの関数の使用をやめ、このような変換が不要なマルチバイト文字列を直接受け取るAPIに置き換えました。またwcs2mbsの戻り値がなぜかconst char*になっていたので、それもchar*に変更しました。
- Shiro(2008/04/21 06:56:58 PDT: mallocが使われ得るのはgauche-config.cだけであり、
gauche-config.cがこれらの関数を呼び出す回数の上限は決まっているので、
ここはリークしても構わない、という意図だったような気がします。
「*A」バージョンのAPIの直接呼び出しというのはofficialに許容されているのでしょうか?
許されているなら直接「*A」バージョンのAPIを使っちゃった方が確かに簡単になりますね。
あと、wcs2mbsの戻り値がconst char*だと何か問題出ますか?
immutableな文字列とみなして欲しい、っていう意図でわざとそうしてあったように
思います。
- cielacanth: 公式にサポートされているかはちょっと分かりません。ただウィンドウズの主要なバージョン(win95/98/NT/2000/ME/XP/Vistaなど)では、LoadLibraryで動的に呼び出すことがある関係で確実にサポートされます。また、boostでもA系の関数を直で呼び出してたりするので、多分大丈夫ではないかと思います。あと、戻り値に関してはそういう意図があるとは知りませんでした。const付でも特に問題はでません。ただ、元のソースではwcs2mbsの戻り値がconst付き、mbs2wcsの戻り値がconst無しなので、どちらかに合わせた方がいいと思います。
- cielacanth: いろいろな事情があったのですね。了解しました。
- 静的初期化周りの問題について(多分must): すごく話が複雑なので、とりあえず一段楽してから話します。
以上の修正を加えたパッチをここにおいて置きます。
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
- libgauche.dll/libext.dllを作成する場合は以下のようなルールを適用する必要があります
- libgauche.dll 本体のソースについては SCM_EXTERN = export
- libext.dll が gauche.h をインクルードする場合は SCM_EXTERN = import
- libext.dll 自身が作成するAPIについては SCM_EXTERN = export
- 実行ファイルが gauche.h をインクルードする場合は SCM_EXTERN = import
- 実行ファイルが libext.dll の関数をインクルードする際は SCM_EXTERN = import
- 実行ファイル自身がgauche関数を作成する場合は SCM_EXTERN = [空]
- まとめると
- libgauche.dll自体を作成する場合や、libext.dll自身がエクスポートする関数/変数については SCM_EXTERN = export
- そのDLLの作成者以外が gauche.h や ext/xxx.h をインクルードする場合は SCM_EXTERN = import
- 実行ファイル自身がgauche関数を作成する場合は SCM_EXTERN = [空]
となります。これだけでも十分おなかいっぱいになれそうなのですが、静的リンクの場合も併せると事態はさらに面白いことになります。特に変数を直で使う場合には、使う側がSCM_EXTERN=importを明示しないといけないのが痛いところです。
現在のクラスエクスポート方式について
ここではScm_StringClassを例に扱っています。
- libgauche.dllからクラスオブジェクトをエクスポートするため、まずScmClass *Scm_StringClassをエクスポート属性で定義し、次にScmClass *_imp__Scm_StringClass(= &Scm_StringClass)をextern属性で定義しています。
このようにすることで、dllからエクスポートされたScm_Stringのアドレスが定数として扱えないことを避けているようです。この実体は普通、exeやdllなどの「モジュールごと」に必要になります。
- libext.dllではそのエクスポートされたScm_StringClassをインポートしています。その際、dll内でextern属性の_imp__Scm_StringClassも一緒に宣言されるのでこの実体がどこかに必要になるはずです。しかし、それがソース内には一切見当たりません。また、リンクエラーも起こりません。
最初この理由がわからなかったのですが、その原理はこのようになっているようです。
- (おそらくwindowsの)仕様で、DLLからインポートされたシンボルは常にその先頭に'_imp__'の文字がつく。
- DLLからインポートされたパブリックデータは、その実アドレスがコンパイル時に決まらないなどの理由で、普通はポインタとして扱われる。
- つまり、DLLからインポートされるオブジェクトType objは普通Type *_imp__obj(= &obj)として扱われる。
- これはgaucheが内部でやっていることと全く同じである。
- つまりlibext.dllが自動的にScmClass *_imp__Scm_Stringの実体を作成するため、これを宣言しただけでリンクエラーが出ることも、エラーが出ることもなく使用できた。
最初このハックを知ったとき驚いてしまいました。今でも、よく考えついたものだと感心しています。
このやり方が非常に上手いということは認めます。それは認めるのですが、以下の3つの理由からこれはやめた方がいいと思います。
- 理由1) このやり方が賢すぎるため。私見ですが、将来のVCで確実に動作させることや、コードのポータビリティを確保するためにも、こういった一部のアーキテクチャに依存するようなやり方はやめた方がいいと思うのです。
- 理由2) 古のBorlandCコンパイラではこのやり方が通用しませんでした。この辺りbccの方がより厳密になっているようです。
- 理由3) このやり方はVC8のリリースモードでは上手くいきません。それは以下のような理由によります。
- 先ほど説明した動作は、少なくともVC8では「デバッグモード」のみ
- リリースモードでは、_imp__Scm_StringClass は Scm_StringClass と全く同じアドレスを有するオブジェクトとなっているようです。実際、デバッグモードではDWORD PTR [_imp__Scm_StringClass]と記述されているところが、リリースモードではoffset _imp__Scm_StringClassとなっています。
理由は正直良く分からないのですが、リリースモードでは最初から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++でコンパイルする」という方法を取るなら、libgauche.dll自体もC++としてコンパイルする必要がある
- 例外として、VC8では古い宣言方式がC言語でコンパイルした場合しか使えないため、gcのソースはC言語としてコンパイルする必要がある
- Cよりもかなり厳密な型キャストが必要
- 名前装飾に気をつける必要がある
が必要でした。また、
- エクスポートする関数の実体かそのソースがインクルードしたプロトタイプが__declspec(dllexport)を含む必要がある
- 拡張関数では、拡張関数自身のデータはexport定義を、そうでなければimport定義を使う
- _UNICODEの定義のいかんに関わらずコンパイルできた方がよい
ということに気をつける必要があります。
(「現在の言語はCかC++か?」「C++ならextern "C"は定義されているか」「ヘッダをインクルードしたときのSCM_EXTERNの定義は何か?」「ソースのSCM_EXTERN定義は何か?」「_UNICODEは定義されているか?」などを適切に判断していけばOKです)
具体的には?
- gcのソースに関して: 先ほども言ったとおりVC8では古い宣言方式がC++では使えないため、gcのソースはC言語としてコンパイルする必要があります。これはちょっとした小細工で済みます。
- 型キャストに関して: 90%はただただキャストするだけでどうにかなります。ただ、一部のソケット関数はwindowsとunix系で引数の型が違うので、その辺はちょっと考えどころだと思います。
- マングリングに関して: 大抵SCM_DECL_BEGIN / SCM_DECL_ENDでどうにかなります。ただgaucheの場合、すべてのヘッダがこれを定義しているわけでは無いので、名前装飾が問題になることがあります。例えばcode.hで定義されている'Scm_VMExecuteToplevels'ですが、これはプロトタイプがgauche.hでは無くcode.hにあり、かつヘッダにSCM_DECL_XXXを定義していないため、マングリングの問題が発生することになります。具体的にはエクスポートされる関数の名前がC++用に装飾されてしまっています。おそらくご存知だと思いますが、これではそれをコンパイルしたコンパイラと同じものでしか使うことができません。またVC8ではboehmライブラリがC++としてコンパイルできないため(^^)、'gc.h'以外のGC関連ヘッダをインクルードする場合は事前にSCM_DECL_XXXを定義しておく必要があります。他にも、拡張ライブラリにある'Scm_Init_xxx'関数を無装飾で出力するために、これらの関数の実体かプロトタイプをSCM_DECL_XXXで囲む必要がありました。
- VCのためのお願い: gaucheでは'gauche.h'以外には基本的に SCM_DECL_BEGIN(END) がついていないため、すべての関数がほんとうに無装飾でエクスポートされるかの保証がありません。保証が無いどころか、今のソースでは'gauche.h'とそこからインクルードされる関数しか無装飾でエクスポートされません。こういったことをちゃんとやるなら、必要なヘッダにSCM_DECL_BEGIN(END)定義をつけて、リリース毎にちゃんとコマンドで調べるのが筋だとは思うのですが、これをいちいちやるのは大変に面倒なわけです。しかも、こういういった作業は往々にして忘れられてしまいがちです。なので、ここで、ちょっとした提案をしたいのですが、、、 gauche.hから直接インクルードされるヘッダを除いて、ヘッダには原則的にSCM_DECL_XXXをつけるようにしてはもらえませんか? そうしてもらえると、このチェックが実質的にいらなくなるのでとても助かるのです。不具合が起こったときの原因究明も楽になります。気分的に抵抗があるのは分かるのですが、検討して頂けるととてもありがたいです。
- エクスポート定義について: 先ほども書きましたが、これには以下のルールを守る必要があります
- libext.h は
- libextプロジェクト内のソースから呼ばれた場合は SCM_EXTERN = export
- プロジェクト外のソースから呼ばれた場合は SCM_EXTERN = import
- libext.c は
- 'gauche.h'や'ext2/xxx.h'など自分のプロジェクトに含まれないファイルについては SCM_EXTERN = import
- 自分のプロジェクトに含まれるヘッダに関しては、対処がなされていると仮定しているので特に考える必要は無い
- 自身のオブジェクトを確実にエクスポートするため、すべてのファイルのインクルードが終わったら SCM_EXTERN = export
- これを実現するにはいくつかの方法があるのですが、とりあえずそのいくつかを紹介します(LIBEXT_EXPORTS はプロジェクトファイルで定義されていると仮定しています)
// 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が二重定義されたり定義されなかったりすることはなくなります。
まあ、とってもメンドクサそうっていのは直感的に分かりますし、不具合が起こったときの対処が非常に難しくなる可能性もあります。
- 方法2) SCM_EXTERN にあたるマクロを各モジュールごとに用意する。
これは比較的多くのライブラリで採用されている方法なのですが、具体的にはこのようにします。
// この拡張ライブラリの基本ヘッダ
// 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定義が必須になってしまうことです。
- 補足1: Shiroさんがどのようなやり方を好むのか分からないので、パッチはまだ用意していません。
- 補足2: 参考までに、自分がC++でコンパイルしたとき出会ったDLL関連エラーを書いておきます。
- 所望のシンボルがdllにもlibにもエクスポートされない
- dllにはエクスポートされたが、libにエクスポートされない
- 装飾付きでエクスポートされたので、Scm_Init_xxxが呼び出せない
- 一部の関数が装飾付でエクスポートされたdllを作成したが、そのときはそれをたまたま見つけられず、
cygwinにインストールしてあるgauche.dllを読み込んでしまった。そしてその装飾付の関数を呼び出そうとして失敗した。
議論
__imp_について
Shiro(2008/04/24 07:42:55 PDT): やあ、詳細な分析ありがとうございます。
ご指摘のとおり、gaucheが__imp_を使ってるのはVCがDLLのimport/export時に
やっていることをエミュレートしているものです。
しかし、こんなことをしなくてもリンカががんばれば外部DLLに定義されている
データをコンパイル時定数として扱うことはできるはずで(現にgccではやっている)、
将来のVCでこのハックが必要無くなる可能性も高いでしょう。
- cielacanth: どもどもです。まず、windowsのdllではベースアドレスが固定という絶対的な制約があるので、将来のVCでこれが解決される可能性はほとんど無いと言っていいと思います。
それどころか、将来バージョンが上がるたびに別の対策が必要になる可能性もあります(実際、この辺りに厳格なBorlandCではコンパイルできません)。
それがあるべき姿のはずで、今のハックはarcaneなVCの仕様に嫌々
付き合っているようなものなので、不要になればそれに越したことはないです。
そういう意味ではあくまでtransientなハックなので、
動きさえすればdirtyであっても構わないと考えています。
この観点からは、当面解決すべきはreleaseビルドを何とか通すことですね。
私のアジェンダとしては、
- staticなオブジェクトアドレスを使うことは維持したい
- windows特有のハックがコードのあちこちに分散することを避けたい。
でも一ヶ所にまとまってさえいればどんなに汚くても構わない。
ってとこですね。ところでReleaseビルドにすると、外部DLLで定義された
Scm_FooClassのアドレスがコンパイル時定数扱いになるんでしょうか。
そうだとしたら__impハックをDebugビルド限定で使うってのはありかもしれません。
でなかったら__scmimpみたいに自前で間接ポインタを用意する方向ですかね。
- cielacanth: インポートされたアドレスが実際の定数かどうかに関わらず、これがコンパイルできることは多分無いです。Debug版とRelease版で定数の扱いが変わるというのも妙な話ですし、自分はVCの仕様だと理解しています。
また、Debug版とRelease版でやり方を変えるというのはなるべく避けて頂きたいのです。これらの動作は僅かなコンパイルオプションの差で変わってしまうと思うのですが、それにいちいち追随して「この場合はこっち。この場合はそっち」みたいなことをするのは大変にめんどうです。また、本質的でもありませんし。
- cielacanth: __scmimpを用意するのでも構わないのですが、全体をC++としてコンパイルするのではダメなのでしょうか? これが可能ならばいちいちVCの仕様に付き合う必要もありませんし(C++の仕様には付き合うことになりますが)、一番楽なのではないかと思う次第です。
- cielacanth: アジェンダの件については了解しました。なるべくその方針で考えてみます。
Shiro(2008/04/25 04:19:59 PDT): ああ、それならC++でコンパイルするのでいいと思います。
gcについても、7.x系列はANSI宣言になっているはずなので。7.1が正式に出たら
置き換えようと思っています。
- cielacanth: どもどもです。了解しました。そのうちパッチを作成します。
EXTERNマクロについて
どちらの方法を取るにしても、私が問題だと思うのは、
- LIBEXT_EXPORTSだとかSCM_EXT_EXTERNだとかのマクロ定義を、EXTごとに
行う必要がある。つまり、これらのマクロ定義自体をすべての拡張モジュールで
使えるような形で抽象化して単一のインクルードファイルに入れておくことができない。
- これらのマクロは純粋に処理系の都合でつけなければならないものであって、
コードの本質的な意味には関係ない。コードを読む人間にとってはノイズでしかない。
"Programs must be written for people to read, and only incidentally for machines to execute."の
精神に立ち返るなら、こういう非本質的なコードは人間が見えるところに
置くのではなく機械に生成させるべきです。なのでいっそのこと、
VC用にソースをプリプロセスする際にVC専用のヘッダファイルを生成する方が
すっきりするような気がします。
- cielacanth: 「読むために書かれるべき」というのには全く賛成です。ただ、ネットワークの世界には「送るときは厳格に、受ける時は寛容に」みたいな言葉もありますし、この辺り、文句を言ってもしょうがないような気がします。
また、ヘッダのプリプロセスというのはいいアイディアだと思います。このやり方でも構わないと思うのですが、他にも、Shiroさんのアジェンダを元に
// 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): 以前言ったパッチを作成いたしました。内容に重複がありますが改めて書きます。主な変更点は以下のとおりです。
- 変数のキャストを厳密に行いました。それに伴い、ソース生成ファイルの方にも変更を入れています。
- (全部直すのはどうかと思ったので、目に付いたところだけ)WinAPIの*W系の使用をやめ、*A系を使うようにしました。(*1)
- intNN_tの使用をやめ、ScmIntNNを使うようにしました。
- 適宜ヘッダにSCM_DECL_BEGIN(_END)を入れました。
- GAUCHE_BROKEN_LINKER_WORKAROUND の使用をやめ、C++でコンパイルするようにしました
- templateという変数をtmplに置き換えました。(C++でコンパイルできないため)
- 周りから使われている構造体内構造体の定義を外に出しました。(C++でコンパイルできないため)
- Scm_WCS2MBS(Scm_MBS2WCS)を常に定義するようにしました。
- Scm_WCS2MBS(Scm_MBS2WCS)とSCM_WCS2MBS(SCM_MBS2WCS)の定義を適宜置き換えました。
- mbs2wcs(wcs2mbs)関数内でのmallocの使用を止めました。
- 宣言されているがVCでは実体が無いクラスに、コメントを入れました
- gaucheがインストールされたディレクトリを探すとき、libgauche.dllが無かった場合にエラーで停止するのをやめました。(*2)
- 'default_exception_handler_rec'変数が複数の場所でstatic宣言されていたため、名前を少し置き換えました。
- 拡張ライブラリのヘッダに細工をしてマングリングの問題を避けるようにしました。そのため"gauche/extend.h"も少し変更しています(これは完璧ではありません *3)
- http://d.hatena.ne.jp/softether/20041201 :win9x系では*W系の関数がことごとく使えないそうなので、この辺全部置き換えた方が安心かもしれません。
- windowsではリリースモードでコンパイルしたdllに'xxx.dll'、デバッグモードでコンパイルしたdllに'xxxd.dll'や'xxx_d.dll'などの名前を付けることが一般的によく行われているためです。これがエラーになるのは少し困るのです。
- 拡張ライブラリの関数/変数のエクスポートのやり方がまだ決まっていないので、この辺り適当にお茶を濁しています^^
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でもバージョンによって微妙に違ったりするので、そのあたりが関係あるのかなぁ。
少なくとも自分の環境では再現しませんでした。ちょっと分かりません。すいません。
- VS2005 SP1,svn HEAD gosh(cygwin)で./DIST winvcしたものでコンパイルで再現しました。
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' の前の定義を確認してください
Last modified : 2012/11/26 00:50:40 UTC