GSLをVisualC++でビルドする

出典: Wikimura

CygwinやMinGWを使えばWindowsでもビルドできるが、VisualC++から直接利用することができない。

MSYSとMinGWでGSLをビルドしてVisual C++から使うでGSL 1.13をMinGWでビルドし、Visual Studioから利用してみたところ、以下のような簡単な関数呼び出しでランタイムエラーに見舞われた。

関数から抜け出す際にスタックの状態がおかしくなるらしい。

#include <gsl/gsl_vector.h>
#include <stdio.h>

int main( void)
{
	double array[3] = { 1, 2, 3};
	gsl_vector_view view = gsl_vector_view_array( array, 3);
	return 0;
}
Run-Time Check Failure #0 - The value of ESP was not properly saved across a function call. 
This is usually a result of calling a function declared with one calling convention with
a function pointer declared with a different calling convention.

これは、VisualC++とMinGWのgccとの呼び出し規約(calling convention)に違いがあるためらしい。 これを解決する方法を模索した。


目次

方針

まず、呼び出し規約の問題を解決するには、Visual C++のコンパイラ cl でコンパイルするのが一番良い方法だと考えた。 呼び出し規約という低レベル(機械語に近いということ)の仕様に標準は無く、コンパイラごとに異なる。 たとえ同じ呼び方がされていても、中身は異なる [1]。 (calling convention cl MinGW等で検索する)

呼び出し規約の問題をラッパーで解決することは不可能ではない。 しかし、手作業になるためミスが発生する可能性がある上、将来の変更に弱い。 更に、高級言語のレベルで全く正しいコードであっても、機械語のインターフェース不一致により、不可解な実行時エラーに遭遇する。 これは安心して使えないため、好ましくない。

コンパイル以外のバイナリ操作(ライブラリを作る部分)は、cygwinのbinutilsを使用するのが最良と考えた。 Makefileなどのビルドツールは、unixのツールを念頭に置いたコマンドが使用されている。 Visual Studio付属のバイナリ操作ツールは、unixのものと使い方が大きく異なるため、そのまま置き換えられない可能性が高い。 できたとしても、Makefileを大きく変更することになり、保守が困難になる。

なるべく最小限の変更で、clでのコンパイルを目指す。


準備: clでコンパイルするには

MakefileをVisual Studioのプロジェクトに変換できるか、Makefileからclを呼び出せれば、問題は解決する。 ただ、(ここに来るまで何度もビルドに挑戦したから分かるが)GSLはMakefile以外にlibtoolというスクリプトを使用し、ライブラリをビルドしている。 そのため、たとえMakefileをプロジェクトに変換できても、その先に進めない可能性がある。 できれば、GSLが元々想定しているやり方に則って、コンパイラだけclに置き換わって欲しい。

Makefileからclを呼び出す方法について検索していたところ、 [2] の書き込みが目に留まった。

You can also use cccl with make for windows.
cccl is a wrapper around Microsoft Visual C++'s cl.exe and link.exe. It converts Unix compiler parameters into parameters understood by cl and link.

ccclというツールは、unix用コンパイラオプションをcl用オプションに変換してくれるらしい。 マニュアルによれば、configureの実行前に、CC(Cコンパイラを指定する変数)にccclを指定することで、configureがコンパイラとして認識してくれるようになるとのこと。

#変数の代入に使うイコールの前後は空白をつけないこと

CC=cccl
CPPFLAGS="/GR /EHsc"
./configure --prefix=インストール場所


必要なもの

Cygwinで以下を入れておく。他にも必要なものはあるが、大抵デフォルトでインストールされているはず。

  • gcc(ccclのコンパイルに使うだけ)
  • binutils
  • autoconf
  • patch
  • wget(ソースダウンロードに。あれば便利)


ccclをビルド

まずcygwin上でビルドしなくてはならない。SourceForgeから持ってくる[3]。 configureで以下のようにインストール先を設定した。 パスが通っているところにあると、「CC=cccl」と指定できる。

tar xvf cccl-0.03
cd cccl-0.03
./configure --prefix=/usr/local/bin
make install


以降の作業について

以降の作業は、主にGSLのソースを展開したディレクトリ内で行う。 このディレクトリは、以降(gsl_root)と表記する。 まずはソースをダウンロードし、展開しておく。

wget ftp://ftp.gnu.org/gnu/gsl/gsl-1.13.tar.gz
tar xvf gsl-1.13.tar.gz
cd gsl-1.13

configure/makeの前に、以降のエラーの予防策を講じる。


予防: linkでエラー

初期状態では、Visual C++をインストールしてあっても、コマンドプロンプトからclやlinkを実行できない [4]を参考に、環境変数の設定を行う。 これを実行することで、PATHやINCLUDEなどが設定される。

Windows上で作業するだけなら、コマンドプロンプトを起動し以下のコマンドを実行すればよい。

  • (VCInstallDir): VisualC++をインストールしたディレクトリ(VisualStudioの下にあるはず)。環境に合わせて置き換えること。
(VCInstallDir)\bin\vcvars32.bat

コマンドプロンプトでvcvars32.batを実行し、Cygwin.batでcygwinを起動することで、Cygwin上からもclが呼び出せるようになる。 ところが、いざmakeをすると以下のようなエラーが起こる。

link: extra operand `./.libs/libgslsiman.lib'
Try `link --help' for more information.


原因: linkコマンドの競合

bash起動時、ログインスクリプトは既存のPATHをunix用に変換してくれる。 予めvcvars32.batを実行しておけば、Visual StudioのツールをCygwinから利用できるようになる。 これと同時に、PATHの先頭に「/bin」や「/usr/bin」といった、unixでお馴染みのパスも付け加えられる。

ここで問題が起こる。Visual C++のリンカlinkと同名のコマンドがCygwinに存在する。 このまま進むとCygwinのlinkコマンドでリンクを行おうとしてしまう。 その結果が前述したエラーメッセージとなる。 試しにヘルプを呼び出すと、間違いに気付くことになる。

link --help
Usage: /bin/link FILE1 FILE2
  or:  /bin/link OPTION
Call the link function to create a link named FILE2 to an existing FILE1.

      --help     display this help and exit
      --version  output version information and exit

Report bugs to <bug-coreutils@gnu.org>.  <--- !?


対処: linkへシンボリックリンクを張る

予めlink.exeの場所を調べておき、/usr/local/binなどの、cygwinのlinkより優先度の高いところにシンボリックリンクを作る。

これは手作業でやることが可能だが、後述するfindlink.batのようにスクリプトにlinkの場所を調べさせた方がより確実となる。 Cygwinルートにfindlink.batがある場合、以下のようにする。 リンクは/usr/local/bin/linkに設置することで、cygwinの/bin/linkより優先度を高くする。

ln -s "`cygpath -u "\`/findlink.bat | dos2unix\`"`" /usr/local/bin/link
  • findlink.batはlinkの場所をSTDOUTに出力する
  • findlink.batの出力をcygpathで変換する(cygpathはパイプでつなげないらしい)
  • dos2unixが無いと、余計な改行文字が悪さをするらしい
  • cygpathの結果は空白を含む可能性があるので、""で囲む
  • シンボリックリンクは次の起動から有効になる?

findlink.bat

コマンドのフルパスを得るには、unix系ならwhichなどが用意されている。 しかしWindowsには相当するものが無い。 [5]を参考に、linkの場所を探すバッチファイルを作る。

@echo off
call "C:\Program Files\Microsoft Visual Studio 9.0\VC\bin\vcvars32.bat" > NUL
call :which link
exit /b

:which
for %%I in (%1 %1.com %1.exe %1.bat %1.cmd %1.vbs %1.js %1.wsf) do if exist %%~$path:I echo %%~$path:I
exit /b


bash起動前にvcvars32.batを自動で起動する

毎回コマンドプロンプトからvcvars32.batとCygwin.batを実行するのが面倒な場合、 bash起動前にvcvars32.batを起動するようにしておく。

vcvars32.batをbash起動前に実行するには、CygwinインストールディレクトリにあるCygwin.batを以下のように編集する。 (vcvars32.batが出力する文字が気になるなら、「> NUL」を使ってSTDOUTをNULへリダイレクトすればよい)

Cygwin.bat

@echo off
中略

call (VCInstallDir)\bin\vcvars32.bat > NUL     <-------- 追加
bash --login -i


予防: ヘッダが見つからない

このままconfigure/makeをすると、以下のエラーに見舞われる。

minmax.c(26) : fatal error C1083: Cannot open include file: 'gsl/gsl_minmax.h':
No such file or directory

原因: clはシンボリックリンクが読めない?

GSLでは、ヘッダがモジュール(vectorやblockといった名前のディレクトリ)ごと分けられている。 make時に、これらのヘッダへのシンボリックリンクが、(gsl_root)/gsl 内に作成される。 全てのソースは「gsl/gsl_vector.h」のように、ルートからgsl以下のヘッダを取り出すように書かれている。

ところが、cl はシンボリックリンクをヘッダとして読めないらしい。 そのため、ヘッダが見つからないといわれてしまう。

対処: ヘッダをコピーする

使用するヘッダを全てどこかにコピーする。 configure前にコピーした場所へのインクルードパスをCPPFLAGSに設定する。

インクルードパスはWindowsの形式で記述すること。また、絶対パスの方が確実で良い。 「\」記号を含むので、外側をで囲む。

ここでは、(gsl_root)/headers/gsl 以下にヘッダをコピーする。 コピーする対象は、(gsl_root)/gsl*.h と、(gsl_root)/*/gsl*.h となっている。 これは、make実行時にシンボリックリンクを作成する作業を見れば分かる。

cd (gsl_root)

mkdir headers
mkdir headers/gsl
cd headers/gsl
HEADERLIST="../../gsl*.h ../../*/gsl*.h"
for header in $HEADERLIST
do
	BASENAME=`basename $header`
	cp $header $BASENAME
done


予防: シンタックスエラー

このままconfigure/makeを実行すると、おかしなところでシンタックスエラーになる。 よく定義していない型を使ったときに見かけるタイプのエラーが出ている。

libtool: compile:  cccl -DHAVE_CONFIG_H -I. -I.. -I.. /GR /EHsc /I..\\headers -g -c oper.c  -DDLL_EXPORT -DPIC -o .libs/oper.obj
cl /nologo -DHAVE_CONFIG_H -I. -I.. -I.. /GR /EHsc /Zi /c oper.c -DDLL_EXPORT -DPIC /Fo.libs/oper.obj oper.c
(gsl_root)\vector\oper_complex_source.c(21) : error C2143: syntax error : missing ')' before '*'
(gsl_root)\vector\oper_complex_source.c(21) : error C2143: syntax error : missing '{' before '*'
(gsl_root)\vector\oper_complex_source.c(21) : warning C4228: nonstandard extension used : qualifiers after comma in declarator list are ignored
(gsl_root)\vector\oper_complex_source.c(21) : error C2143: syntax error : missing ';' before '*'
(gsl_root)\vector\oper_complex_source.c(21) : error C2059: syntax error : ')'
(gsl_root)\vector\oper_complex_source.c(22) : error C2054: expected '(' tofollow 'b'


原因: プリプロセッサの不具合

GSLでは、テンプレート風なことをCでやるために複雑なことをしている。 エラーの起きたoper.cは、型ごとにoper_source.cやoper_complex_source.cをインクルードすることで、型ごとの関数を生成する。

oper.c 一部

#define BASE_GSL_COMPLEX_LONG
#include "templates_on.h"
#include "oper_complex_source.c"
#include "templates_off.h"
#undef  BASE_GSL_COMPLEX_LONG

#define BASE_GSL_COMPLEX
#include "templates_on.h"
#include "oper_complex_source.c"
#include "templates_off.h"
#undef  BASE_GSL_COMPLEX

cl -E で、oper.cをプリプロセスのみ実行したところ、2つ目「BASE_GSL_COMPLEX」のマクロ展開結果がおかしいことが分かった。 以下のように、アンダースコアが冗長になってしまっている。 gsl_vector__complexという型が無いため、コンパイルに失敗するらしい。

更に調べたところ、complexというシンボルが「_complex」に置き換わっていることが分かった。 どういう経緯でこうなったのかは不明。


clでプリプロセス

int 
gsl_vector__complex_add (gsl_vector__complex * a, const gsl_vector__complex * b)
{
  const size_t N = a->size;
以下略


同じソースをgccでプリプロセスのみ行ったところ、以下のようになった。冗長なアンダースコアは生じなかった。

gccでプリプロセス

int
gsl_vector_complex_add (gsl_vector_complex * a, const gsl_vector_complex * b)
{
  const size_t N = a->size;
以下略


対処A: 手動でcomplexをundefする

template_on.hはあちこちで使われている。 operだけ対策を施してもしょうがないので、根元を断つ。

以下のように、BASE_GSL_COMPLEXが定義された場合のマクロ定義の部分で、complexシンボルをundefする。 これにより、余計なアンダースコアが付くのを防ぐことができた。 (この副作用は今のところない)

template_on.h(一部)

#elif defined(BASE_GSL_COMPLEX)
#define BASE gsl_complex
#undef complex
#define SHORT complex
#define SHORT_REAL
#define ATOMIC double
#define MULTIPLICITY 2
#define FP 1
#define IN_FORMAT "%lg"
#define OUT_FORMAT "%g"
#define ATOMIC_IO ATOMIC
#define ZERO {{0.0,0.0}}
#define ONE {{1.0,0.0}}
#define BASE_EPSILON GSL_DBL_EPSILON


対処B: パッチで直す

(gsl_root)以下に、templates_on.h.patchを作成し、以下のコマンドを実行する。

patch < templates_on.h.patch

templates_on.h.patch

*** templates_on.h.old
--- templates_on.h
***************
*** 21,26 ****
--- 21,27 ----
  
  #elif defined(BASE_GSL_COMPLEX)
  #define BASE gsl_complex
+ #undef complex
  #define SHORT complex
  #define SHORT_REAL
  #define ATOMIC double


予防: オブジェクト名の重複

このまま進むと、今度は以下のようなエラーに遭遇する。 アーカイブ内のオブジェクト名が重複しているらしい。

libtool: link: (cd .libs/libgsl.lax/libgslblock.lib && ar x "/cygdrive/e/dev/gsl-1.13/block/.libs/libgslblock.lib")

libtool: link: object name conflicts in archive: .libs/libgsl.lax/libgslblock.lib//cygdrive/e/dev/gsl-1.13/block/.libs/libgslblock.lib
make[2]: *** [libgsl.la] Error 1
make[1]: *** [all-recursive] Error 1
make: *** [all] Error 2


原因: アーカイバlibとarの違い

GSLはビルドの際に、libtoolというスクリプトを利用する。 このスクリプトは、configureで生成される。

libtoolは以下のコマンドでオブジェクトコードをまとめる。 その中身は階層構造が保たれていることが分かる。

libtool: link: lib -OUT:.libs/libgslblock.lib .libs/init.obj .libs/file.obj .libs/block.obj

内容を確認: 
lib libgslblock.lib /LIST

.libs/block.obj
.libs/file.obj
.libs/init.obj

一方binutilsのアーカイバarはフラットに扱う。arは階層構造を持ったものは扱えないために、libの作った物は以下のように見える。 全部同じに見えてしまうために、エラーが起こったらしい。arで内容を見ると以下のようになる。

ar t libgslblock.lib
.libs
.libs
.libs

libの代わりにarを使ってアーカイブすると以下のようになる。 こうなれば、コンフリクトが起きない。

ar cru .libs/libgslblock.lib .libs/init.obj .libs/file.obj .libs/block.obj

ar t .libs/libgslblock.lib
init.obj
file.obj
block.obj


対処: libtoolのアーカイブ作成コマンドをarに置き換える

アーカイブ作成コマンドは、libtoolの変数old_archive_cmdsで設定されている。 ここでlibを使わず、arで同等のことをするようにすれば、前述したようにオブジェクト名の衝突が無くなる。

オブジェクト単位のバイナリ取り扱いなら、コード本体に影響を及ぼさない。 Visual Studioのツールを gnu のツールに置き換えても影響は無い(はず)。

libtoolはconfigureが自動的に生成する。 そのため、configure前にはlibtoolは存在しない。 生成されたlibtoolを編集しても、configureし直すとやり直しになってしまう。

そこで、元となるconfigureを編集する。 libtoolでlibコマンドを使うよう設定している記述は以下のようになっている。 FIXMEと書いてある。


configure(一部)

    cygwin* | mingw* | pw32* | cegcc*)
      # When not using gcc, we currently assume that we are using
      # Microsoft Visual C++.
      # hardcode_libdir_flag_spec is actually meaningless, as there is
      # no search path for DLLs.
      hardcode_libdir_flag_spec=' '
      allow_undefined_flag=unsupported
      # Tell ltmain to make .lib files, not .a files.
      libext=lib
      # Tell ltmain to make .dll files, not .so files.
      shrext_cmds=".dll"
      # FIXME: Setting linknames here is a bad hack.
      archive_cmds='$CC -o $lib $libobjs $compiler_flags `$ECHO "X$deplibs" | $Xsed -e '\''s/ -lc$//'\''` -link -dll~linknames='
      # The linker will automatically build a .lib file if we build a DLL.
      old_archive_from_new_cmds='true'
      # FIXME: Should let the user specify the lib program.
      old_archive_cmds='lib -OUT:$oldlib$oldobjs$old_deplibs' <--- ここを書き換える
      fix_srcfile_path='`cygpath -w "$srcfile"`'
      enable_shared_with_static_runtimes=yes


これを「ar cru」を使うように書き換えればよい。 書き換えには後述するconfigure.patchを利用する。 (gsl_root)以下にconfigure.patchを置き、以下のコマンドを実行する。

patch configure.patch


configure.patch

*** configure.old
--- configure
***************
*** 8295,8301 ****
        # The linker will automatically build a .lib file if we build a DLL.
        old_archive_from_new_cmds='true'
        # FIXME: Should let the user specify the lib program.
!       old_archive_cmds='lib -OUT:$oldlib$oldobjs$old_deplibs'
        fix_srcfile_path='`cygpath -w "$srcfile"`'
        enable_shared_with_static_runtimes=yes
        ;;
--- 8295,8301 ----
        # The linker will automatically build a .lib file if we build a DLL.
        old_archive_from_new_cmds='true'
        # FIXME: Should let the user specify the lib program.
!       old_archive_cmds='ar cru $oldlib$oldobjs$old_deplibs'
        fix_srcfile_path='`cygpath -w "$srcfile"`'
        enable_shared_with_static_runtimes=yes
        ;;


ランタイムライブラリの選択

clは何も指定しないと、シングルスレッド用のランタイムライブラリをリンクしてしまう。 GSLを使用する側が /MD(マルチスレッド) オプションを選択していた場合、ライブラリが競合すると言われてしまう。 (大抵、関数の再定義とかでエラーが起こる)

これを防ぐために、clに渡すオプション(CPPFLAGS)で、どのランタイムライブラリを使うかを指定する。 [6]にあるように、 3つのタイプ毎に、releaseとdebug版がある。

  • /MD, /MDd: マルチスレッド
  • /MT, /MTd: シングルスレッド
  • /LD, /LDd: DLL
  • d はデバッグ用

アプリケーションは、大抵Releaseで/MD、Debugで/MDd が使われるので、それに合わせてGSLもReleaseとDebug用に作り分けるとよい。


いよいよconfigure/make

ここまでで恐らくエラーの芽は摘み取れたはず。 後は以下のように、configure/make/make installを行う。

configure前に、コンパイラの指定やオプションの指定を行う。 これらを指定するには、下記の変数をexportする。

  • CC: コンパイラを指定 -> cccl とする
  • CPPFLAGS: cl に渡すオプションを師弟
    • ' 'で囲むと良い
    • ランタイムライブラリを選択すること
    • ヘッダをコピーした場所をインクルードパスに追加すること
      • Windows用の形式で書くこと
      • 絶対パスが良い
    • その他オプションは、cl /HELPで確認すること


configure例

cd (gsl_root)
export CC=cccl
export CPPFLAGS='/MD /arch:SSE2 /O2 /Oi- /I"headersへのパス" '

./configure --prefix=インストール場所
make
make install
  • マルチスレッド(リリース用)ランタイムライブラリを選択
  • SSE2を有効化
  • 速度最大化
  • 内部関数不使用
  • コピーしたGSLヘッダへのパス

確認

この時点でDLLは生成されないが、利用することは可能。 Visual Studioで[プロパティ]->[リンカ]->[追加の依存ファイル]に、生成されたライブラリ「libgsl.lib」と「libgslcblas.lib」を追加する。

これでMinGWでビルドしたライブラリが失敗したプログラムを試してみたところ、エラーは起きなかった。

#include <gsl/gsl_vector.h>
#include <stdio.h>

int main( void)
{
	double array[3] = { 1, 2, 3};
	gsl_vector_view view = gsl_vector_view_array( array, 3);
	return 0;
}

まとめ

  1. cccl をインストール
  2. 環境変数を設定
  3. gslのヘッダをコピー
  4. templates_on.hを修正
  5. configureを修正
  6. CCとCPPFLAGSを設定してexport
  7. configure/make/make install

以上でスタティックライブラリまではできる。 DLLについては今後。


追記

最適化オプション「/O2」について

CPPFLAGSにclへ渡すオプションとして、速度最大化の「/O2」を付け加えるとコンパイルに失敗する。 内部関数(intrinsic function)というのを利用するらしい。 GSLで内部関数と同名の関数を定義しているため、再定義していると言われてしまう。

そこで、内部関数の利用を無効化するために「/Oi-」オプションも追加する。


参考文献

  1. Stdcall and DLL tools of MSVC and MinGW
  2. Unix Makefile in Windows Visual Studio 2008
  3. ccclソース(SourceForge)
  4. コマンド ライン ビルドのパスと環境変数の設定
  5. whichコマンドを作る
  6. /MD、/MT、/LD (ランタイム ライブラリの使用)
個人用ツール