ライブラリを作成、使用する(Windows編)

先日、覚え書きとして「ライブラリを作成、使用する」を書きましたが、同じ事を Microsoft Windows と Microsoft Visual Studio で行うためのメモです。

環境は Microsoft Windows 7 Professional 日本語版 32bit / Microsoft Visual Studio 2010 Professional です。

Linux では共有ライブラリと静的ライブラリでしたが、Windows の世界ではダイナミックライブラリ(.dll)と 共通オブジェクトファイル COFF(.lib)があるようです。後者は、Microsoft の文書中で「COFF形式の標準ライブラリ」とも表記されていますが、C 標準ライブラリとは無関係で、Linux で言う静的ライブラリの事を指すようです。

                       | Linux              | Windows
標準コンパイラ         | gcc                | cl.exe
オブジェクトファイル   | .o                 | .obj
静的ライブラリ         | .a                 | .lib
共有ライブラリ         | .so                | .dll
インポートライブラリ   |                    | .lib
エクスポートライブラリ |                    | .exp
エクスポート定義       | GCC version script | .def
ライブラリ検索パス     | 可変               | 強制
バージョン管理         | 環境変数、リンク   | Side by Side

DLL だけでも、レギュラー DLL、MFC に静的リンクしたレギュラー DLL、MFC に動的にリンクしたレギュラーDLL、MFC 拡張 DLL、COM をサポートした DLL(OLE/ActiveX/COM+)、Microsoft .NET Framework 向けのマネージド DLL など、様々な種類や実装方法があるようですが、最も純粋なレギュラーDLLをベースに書きたいと思います。

作業に取り掛かる前に、開発環境に必要な環境変数を定義する必要があります。Visual Studio をインストールすると、環境変数「VS100COMNTOOLS」が定義されます。これは名前のとおり、Visual Studio 2010 の共通ツールへのパスが定義されています。標準では「C:\Program Files\Microsoft Visual Studio 10.0\Common7\Tools」というパスになっているはずです。Visual Studio 2008 や 2005 でも VS**COMNTOOLS という環境変数が定義されるはずですので、読み替えればOKだと思います。

コマンドプロンプトを立ち上げ、次のようにタイプします。

> "%VS100COMNTOOLS%\vsvars32.bat"
 Setting environment for using Microsoft Visual Studio 2010 x86 tools.

これで開発に必要なツールに PATH が通り、開発ができるようになります。(なお、%VS100COMNTOOLS% には標準で空白文字が含まれているので、上記のようにダブルクォートで囲ってあげないと、変数展開後にパスを見失なってしまうので注意が必要です。vsvars32.bat で定義された環境変数の寿命は、コマンドプロンプトのセッション内ですので、コマンドプロンプトを一度閉じてしまったら、再度、上記のコマンドを実行する必要があります。)

前回と同様、サンプルとして次の func.c を準備します。

[cpp]
int add(int a, int b)
{
return a+b;
}
[/cpp]

二つの整数値を足して、その値を返すだけの簡単な関数 add() を定義しています。

オブジェクトファイルの静的リンク

前回と同様、Microsoft の C コンパイラ cl.exe も -c オプション(コンパイルのみ)をサポートしています。 Microsoft 流儀で言うと /c オプションとなりますが、このオプションはハイフン(-)でもスラッシュ(/)でも通るようです。実際に func.c をビルドすると、静的ライブラリ func.obj が作成できます。gcc では func.o ですが、拡張子が DOS 風に3文字となっています。

> cl -c func.c
 Microsoft(R) 32-bit C/C++ Optimizing Compiler Version 16.00.30319.01 for 80x86
 Copyright (C) Microsoft Corporation.  All rights reserved.

 func.c

Microsoft の著作権情報は /nologo オプションで非表示にすることができます。コマンドラインが長くなりますが… :(

呼び出し元から静的リンクするには、次のように一緒にビルドするだけです。ここはほとんど gcc と変わりませんね。

> cl /out:app.exe app.c func.obj
 Microsoft(R) 32-bit C/C++ Optimizing Compiler Version 16.00.30319.01 for 80x86
 Copyright (C) Microsoft Corporation.  All rights reserved.
app.c
 Microsoft (R) Incremental Linker Version 10.00.30319.01
 Copyright (C) Microsoft Corporation.  All rights reserved.
/out:app.exe
 app.obj
 func.obj
> .\app.exe
 a+b=8

gcc の -o オプションは cl だと /out: になります。省略すると、最初に指定されたファイル名に拡張子 exe をつけたものを吐いてくれますが、ビルドするファイルが多い場合や処理を自動化した場合、予期せぬファイル名を自動的につけて出力する危険性があるので、/out: オプションはなるべく省略しない方がいいと思います。

ちなみに、コマンドプロンプトから実行ファイルの指定を確実にするには、

> .\app.exe

と相対パスで実行する方が無難でしょう。

ダイナミックリンクライブラリを動的リンク

共有ライブラリのようなものとして、ダイナミックリンクライブラリ(DLL)という実装があります。これは、前回の Linux と gcc をやった時のようにコンパイルオプション一つ変更するだけで完成する訳ではなく、専用の記述やファイルを準備しなければなりません。

func.c を次のように修正します。

[cpp]
__declspec(dllexport)
int add(int a, int b)
{
return a+b;
}
[/cpp]

__declspec(dllexport) が関数宣言の前につきました。これは、ライブラリが公開する関数を制御する為に用いられるものらしいです。前回に比べればちょっと面倒くさいですが、そういうものらしいです。

これをビルドします。

>cl /LD func.c
 Microsoft(R) 32-bit C/C++ Optimizing Compiler Version 16.00.30319.01 for 80x86
 Copyright (C) Microsoft Corporation.  All rights reserved.

 func.c

 Microsoft (R) Incremental Linker Version 10.00.30319.01
 Copyright (C) Microsoft Corporation.  All rights reserved.

 /out:func.dll
 /dll
 /implib:func.lib

 func.obj

 ライブラリ func.lib とオブジェクト func.exp を作成中

オプションは /LD です。大文字・小文字を判別しますので注意してください。上手くビルドされると、4つのファイルが生成されます。

 func.dll ... DLL ファイル。
 func.exp ... エクスポートライブラリ。循環参照の解決に用いられるらしい。
 func.lib ... インポートライブラリ。外部から DLL を参照する際に用いられる。
 func.obj ... オブジェクトファイル。

これで DLL ファイルが完成しました。

次は、DEF ファイルを用いて作成してみます。DEF ファイルを用いると、オリジナルのソースコードをライブラリ化の為に変更しなくていいので、func.c を最初のサンプルのように戻します。

[cpp]
int add(int a, int b)
{
return a+b;
}
[/cpp]

そして func.def という新しいファイルを作成し、下記のように入力します。

LIBRARY    func
EXPORTS
     add @1

add 関数を序数1で「エクスポート(つまり公開)」する、という意味らしいです。

DEF ファイルを用いてビルドするには次のコマンドを実行します。

>cl /LD /DEF:func.def func.c
 Microsoft(R) 32-bit C/C++ Optimizing Compiler Version 16.00.30319.01 for 80x86
 Copyright (C) Microsoft Corporation.  All rights reserved.

 func.c

 Microsoft (R) Incremental Linker Version 10.00.30319.01
 Copyright (C) Microsoft Corporation.  All rights reserved.

 /out:func.dll
 /dll
 /implib:func.lib

 func.obj

DEF ファイルが、gcc でいうバージョンスクリプト的なものなら、なんとなく理解できる仕様です。ソースコードと DEF ファイルの管理が煩雑にならないか注意する必要がありますね。

さて、生成された DLL ファイルを利用するには、app.c も改変します。

[cpp]

include <stdio.h>

__declspec(dllimport) int add(int, int);
int main()
{
printf("a+b=%d\n", add(3, 5));
return 0;
}
[/cpp]

dllimport がついた関数宣言を追加しました。ただし、前回と同様に必須ではなく、MSDNでの解説曰く

__declspec(dllimport) を使用しなくてもコードは正しくコンパイルされますが、使用した方がコードがより洗練されます。__declspec(dllimport) を使うと、ある関数が DLL 内にあるかないかをコンパイラが判断できるようになり、その結果、コードでは通常 DLL の境界を越える関数呼び出し内に存在する間接操作のレベルをスキップできます。ただし、DLL 内に変数をインポートするには、__declspec(dllimport) を必ず使用する必要があります。

との事です。

さらにビルド時には、インポートライブラリを渡してやらなければなりません。func.lib を指定していますが、この場合は共通オブジェクトファイルとは全く別のものなので注意が必要です。

> cl app.c func.lib
 Microsoft(R) 32-bit C/C++ Optimizing Compiler Version 16.00.30319.01 for 80x86
 Copyright (C) Microsoft Corporation.  All rights reserved.

 app.c

 Microsoft (R) Incremental Linker Version 10.00.30319.01
 Copyright (C) Microsoft Corporation.  All rights reserved.

 /out:app.exe
 app.obj
 func.lib

 E:\tmp>app.exe
 a+b=8

これで実行バイナリ app.exe が生成されます。
ちなみに、インポートライブラリを使わずに DEF ファイルを用いて「インポート」する方法もあるらしいです。

ダイナミックリンクライブラリを動的ロード

NEC LaVie LightのUbuntuをアンインストール

先日、実家に帰った際、妹がパソコンを欲しいと言ってきたので、それならばと以前メインで使っていたネットブックをお下がりすることにしました。

ネットブックは NEC LaVie Light BL330。タブレットPCが流行する前の、各社が競ってネットブックを出していた頃の機種です。当時の Microsoft の OS 提供ライセンスの関係に基づいてハードウェア仕様に制限をかけてあるらしいので、当時としてもあまり高いスペックではありません。(逆に言うと、その頃に登場した主力メーカーのネットブックはほぼ Windows XP を搭載していたので、どのメーカーのものでも軒並、似たようなスペックでしたが…)

低スペックとは言えど、ちゃんとチューニングをしてあげれば、メールやインターネットは難なく行えると思います。妹は我が家でも一番コンピュータ関連に疎いライトユーザなので、このくらいのスペックでも問題ないでしょう。

今年の5月に、マウスコンピューター製の 4 万円ほどのラップトップを購入して、3万5000円で購入した LaVie Light から移行したのですが、新しい環境は

  • 画面がちょっと広い
  • スペックがちょっと高い

以外は、ほとんど魅力を感じませんでした。LaVie Light の方が、筐体がしっかりしているし、コンパクトで持ち運びにも便利。マウスコンピューター製のラップトップは、筐体の底に手をあてがって普通に持つと、底のプラスチックが自重で歪むのか、中のファンが筐体に擦れる音がなります。どんだけ筐体薄いんだよ、と思いました。筐体が全ての箇所において安っぽいのはさておき、画面がめちゃくちゃ悪く(素人目で分かるほど発色が違い、他のディスプレイで判別できる色の差がこのラップトップで表示すると判別できない、薄い色は飛び、暗い色は潰れる…など。カラープロファイルとかソフトウェア的な調整をしてみたけれど変わらず。)、絵を描いたり、画像処理をする用途にはまったく向いていませんでした。ここは、かなり凹んだと同時に、なんだかんだ言いつつ、結局日本製が安心だなと思いました。

ともあれ、メイン環境は既にマウスコンピューターのラップトップに移行済みでしたので、LaVie Light は寝室の枕元で本と一緒に並んでいました。LaVie Light には Ubuntu を入れて使っていたので、プリインストールされていた WindowsXP に戻したいと思います。

ディスク構成

LaVie Light を購入した直後は、以下のようになっていたと思います。

基本領域1Cドライブ(NTFS) 論理領域1Dドライブ(NTFS) 基本領域2リカバリ領域(FAT32)
拡張領域

物理ディスクは内蔵のSATA一つだけ。MBRにはNTローダが鎮座しており、初期値で基本パーティション1のCドライブを起動するようにしてありました。基本パーティション2のリカバリ領域は、リカバリCD-ROMが付属していない為、ハードディスクにその領域を確保してあるようで、リカバリイメージのほか、リカバリ・ウィザード表示の為の簡易Windows(MiniNT、Windows PE みたいなものらしい)一式とシマンテックのGhostが入っていました。

Ubuntuをインストールした際、リカバリCD-ROMもないと言うことで、リカバリ領域だけは残して、次の構成でインストールしました。

基本領域1swap 基本領域2/boot (ext2) 基本領域3/ (ext3) 基本領域4リカバリ領域(FAT32)

余談ですが、最初はUSB CDドライブを接続してDebian GNU/Linuxをインストールしたのですが、カーネルモジュール(ドライバ)周りを整えるのに心が折れてしまったので、オールインワンなUbuntuにしてしまいました。MBRにはgrub2、ブートエントリには/bootに入ってるカーネルと、自動認識されて追加されたリカバリ領域の2種類がありました。

さあ、この状態からWindows XPに戻します。

修復ウィザードを実行

LaVie Lightの電源を入れ、grubが起動したらリカバリ領域から起動させます。すると、MiniNTが起動して、GUIな修復ウィザードが起動します。ウィザードに従ってハードディスクをフォーマット後、リカバリするようにしました。元通りにしたいので、以下のような構成ですね。(ドライブを細分化する必要もないので、Dドライブは基本パーティションに格上げしました)

基本領域1Cドライブ(NTFS) 基本領域2Dドライブ(NTFS) 基本領域3リカバリ領域(FAT32)

ハードディスクのフォーマットを終えると、一旦再起動するように促されます。手順に従い、再起動させました。

すると、BIOS画面を過ぎた後、ブートローダが起動すべき場所で

[bash]Grub Error (22)[/bash]

と出てOSを起動することができません。半ば予想はしていたのですが、それでも淡い期待をしつつ修復ウィザードに任せた自分がバカでした。

リカバリ領域の修復ウィザードは、MBRまで修復(fixmbr)してくれないみたいです。

grubよ、そりゃあ/boot/grub/grub.cfgが消えてるんだからエラー吐くよね。お前は悪くない。自分のようにカスタマイズして使わない場合でも、マイクロソフトの回復コンソール(とfixmbr/fixbootなど)が起動するCD-ROMはないのだから、MBRが壊れたらマズいんじゃないの…。リカバリ領域を起動できた→つまり、MBRは修復しなくても大丈夫なんだろ?という発想なのかもしれません。

Trisquel GNU/Linuxで起動

さて、どうしたものかと思ったものの、とにかく起動できないのでは手の施しようもないので、ちょうど手元にあったフリーソフトウェア財団のカード型USB KeyをLaVie Lightに差して起動しました。中にはTrisquel GNU/Linuxがプリインストールされています。手元にこういうツールがあったら本当に便利ですね。コイツには何度も助けられています。

USBブートしたTrisquel上で、

[bash]sudo grub-install /dev/sda[/bash]

しても、まあナンセンスだろうと思い、マニュアルで色々するよりは、Trisquelをハードディスクにインストールして、その際に他のパーティションにあるWindows領域のブートエントリまで拵えてくれるくれる方が確実だろう、と、インストールを実行することにしました。

基本領域1Cドライブ(NTFS) 基本領域2Cドライブ(NTFS) 論理領域1/ (ext3) 論理領域2swap 基本領域3リカバリ領域(FAT32)
拡張領域

基本パーティション2を縮めて、空いた領域に論理パーティションを作成、その中に / と swap を確保しました。インストール後、再起動すると見事にgrubが復活してくれました。

基本パーティション1のCドライブのブートエントリも入ってるかなーと、またも淡い期待をしていましたが、論理パーティション1のTrisquelと基本パーティション3のリカバリ領域のエントリしかありませんでした。

というのも、Trisquel側から基本パーティション1をマウントして分かったのですが、Cドライブの中には設定ファイルが一つ入っているだけで、ほぼ空の状態でした。修復ウィザードがMBRを修復することなくハードディスクをフォーマットして再起動した時点では、まだリカバリイメージの展開が行われていなかったことをここで知りました。

リカバリ処理の続きを再開

それならブートエントリが自動的に追加されていなくて然り、この続きの手順が分からないままでしたが、とりあえずgrubが復活してリカバリ領域を起動できるようになったので、起動してみました。すると、今度は修復ウィザードではなくシマンテックのGhostが起動し、ユーザの入力を待たずしてインジケータバーを動かし始めました。あ、ようやくリカバリイメージの展開を始めたな。10分ほど待つとCドライブへの書き込みが終わったようで、自動的に再起動しました。

ここで、MBRも書き換えてくれたかなあ、と半ば諦め気味に起動画面を見ていたのですが、やはりgrubのままでした。ブートエントリはもちろん変わらず。Cドライブのリカバリイメージだけだから、ブートレコードはリカバリイメージに含まれていても、マスターブートレコードまではリカバリの対象外なんでしょうね。

さて、確実にリカバリイメージの転送は終わっているので、Cドライブは起動可能な状態になっているはずです。残るはMBRを戻したいのですが、まずはCドライブから起動できないと話しにならないので、起動してみることにしました。

Grubの設定

Trisquelでインストールされるgrubは、初期設定でパスワードが設定されていました。grubのブートエントリ選択画面でeキーを押しても、起動コマンドの編集の前にユーザ名とパスワードを求められ、編集することができません。

一旦、Trisquelを起動して

[bash]sudo less /boot/grub/grub.cfg[/bash]

してみると、ユーザ名「grub」、パスワード「12953」と設定されていることが分かりました。再起動して、grub画面でこれらの情報を入力すると、コマンドの編集画面に辿りつきました。Windowsのブートコマンドの書き方は知らないので、リカバリ領域に入ったMiniNTのものを変更して用いることにしました。

[bash]insmod fat
set root='(hd0,3)’
search –no-floppy –fs-uuid –set d8fe-92a4
drivemap -s (hd0) ${root}
chainloader +1
[/bash]

1行目はFATじゃなくてNTFSに変更、(hd0,3)はマスターに接続されたディスクの3つめのパーションってことだろうから、(hd0,1)に変更。searchの行は、ディスクを探されるとかえって面倒なので削除して、次のようにしました。

[bash]insmod ntfs
set root='(hd0,1)’
drivemap -s (hd0) ${root}
chainloader +1
[/bash]

C-xを押して起動してみると、ようやくCドライブを起動することができました。WindowsXPの初期設定画面が出てきます。その後、Windowsの初期設定→NECのプリインストールされているソフトの本当にどうでもいい初期設定で4回ほど再起動しやがりました。その度にgrub起動時のパスワード入力からエントリの書き換えまでやりました。Cドライブの起動コマンドをブートエントリに登録しとけばよかったんですが、「1回再起動したらWindowsの初期設定も終わるし、すぐにfixmbrできるだろう」と思ったのが間違いでした。ハードは最高なのにソフトがダメっていう典型的な日本企業のアレですね。本当にどうでもいい初期設定を繰替えしました。

fixmbrはどこ?

WindowsXPに戻せたとは言え、MBRがgrubのままなのでNTローダに差し替える必要があります。回復コンソールが起動できるようなリカバリCD-ROMはないし、リカバリされたWindowsXPにも、もちろんfixmbr.exeは入っていません。Microsoftの公式配布も起動用フロッピーやCDイメージばかりで、あまりこのご時世に合ったサポートは得られそうにありませんでした。そもそもこんな基本的なツールも同梱してないとは…と閉口しつつも、太陽が昇り始めていたので本日の作業はここまでということにしました。

NTローダを諦めれば、Trisquelをもう一度立ち上げて、grubのブートエントリにCドライブのみにして、自動起動までの時間を短くしておけばいいかなあ、と思っています。