読者です 読者をやめる 読者になる 読者になる

Gobble up pudding

プログラミングの記事がメインのブログです。

MENU

Visual C++でのDLLの一般的な作成方法(暗黙的リンク)

スポンサードリンク

f:id:fa11enprince:20150727234208j:plain
2016年02月07日更新
https://msdn.microsoft.com/library/81h27t8c(v=vs.110).aspx
によるとメンバーに対してdllexportはだめらしいので追記。
また、main側サンプルコードが中途半端だったので、C++03以前で書きました。古い環境で開発せざるを得ないこともあるので

今回はVisual C++でのDLLの一般的な作成方法についての記事です。
DLLの作成方法や使い方にはいくつか方法がありますがもっとも一般的なのを紹介します。
Windowsでgcc以外でダイナミックリンクライブラリを作ろうとするとわりと面倒です。
今回は暗黙のリンクと呼ばれる方法と実装を説明します。
※ただしこの方法は個人だけで開発する場合だったり規模が小さい場合だったりの場合で、明示的リンクのほうが適している状況もあります(後述)。

暗黙的リンクの概要

さて、話を戻して、暗黙的リンクについてですが、gccとは違い
__declspec(dllexport)__declspec(dllimport)というMicrosoftの固有実装を使わなくてはいけません。
上記2つは呼出規約というもので、最初はおまじないと思っていてもOKです。気になる方はググれば理解できるでしょう。
DLLを作る側では__declspec(dllexport)を使い、DLLを呼ぶ側では__declspec(dllimport)をつかいます。ヘッダー側でこれをやるのですが、あれ?そうなると.hが2つ必要になって二重メンテじゃ……ってなるんですが、普通はプリプロセッサで回避します(後述)。
一度やるとなんてことはないのですが、結構手順・設定が厄介なので1から順に説明します。

以下はわりと最新のVisual Studio 2013あたりを想定しています。

暗黙的リンクでの実装方法

ソリューションを作成します

ファイル > 新規作成 > 新しいプロジェクト
空のプロジェクトを選び、

名前: BmiDll
場所: 任意
ソリューション名: DllTestSol

としておきます。
f:id:fa11enprince:20140620013349p:plain

ソリューションエクスプローラーでDLLの設定をします

ソリューションエクスプローラー上でBmiDllプロジェクトを右クリックして
プロパティを選びます。
構成プロパティ > 全般を選び、
左上の構成を「すべての構成」にします。
プロジェクトの規定値の構成の種類を「ダイナミック ライブラリ (.dll)」に変更して適用を押します。
f:id:fa11enprince:20140620011434p:plain
構成プロパティ > C/C++ > プリプロセッサ
プリプロセッサの定義の先頭にBMIDLL;を追加します。
f:id:fa11enprince:20140620012737p:plain

クラスを作成します

ソリューションエクスプローラーのテキトーなところで右クリックしてクラスを作成します。
Bmiと名前を付けましょう。
プリプロセッサはBMIDLLが指定されているとき、つまりこのDLL側のプロジェクトでは
__declspec(dllexport)としてビルドして、
DLLを使う側は
__declspec(dllimport)としてビルドされるということを書いています。
publicのメンバ関数にBMIDLL_APIがついていることに注目してください。

DLLのソースコード

Using dllimport and dllexport in C++ Classesによると、
クラスごとdllexportを書くのが正しいようです。どうしても公開範囲を制限したい場合は
専用のインターフェースクラスを作るしかないようです。


いったんビルドします。

下記が出ればOKです。

========== ビルド: 1 正常終了、0 失敗、0 更新不要、0 スキップ ==========

DLLを利用する側の新規プロジェクトを作成します

追加 > 新しいプロジェクト
プロジェクトの名前はなんでもいいですが、
BmiTestMainという名前で作りましょう。
f:id:fa11enprince:20140620013320p:plain

DLLを利用する側のプロジェクトの設定をします

BmiTestMainのプロパティを開いて
構成をすべての構成にして
リンカー > 全般
追加のライブラリディレクトリ

$(SolutionDir)$(Configuration)

これらは事前に勝手に定義されている変数(マクロ)です。
どこで定義されているのかは知りません(;´・ω・)……。なんかのファイルか?
f:id:fa11enprince:20140620015002p:plain
続いて…
リンカー > 入力
を選びます。
追加の依存ファイルの先頭に下記を追加します。
BmiDll.lib;
f:id:fa11enprince:20140620014132p:plain
さらに、今回は面倒なんで、コンソールアプリにします。
実行時にウィンドウがすぐに閉じないようにコンソールアプリ用の設定に変えます。
リンカー > システムのサブシステムを
コンソール (/SUBSYSTEM:CONSOLE)に変えます。
f:id:fa11enprince:20140620014325p:plain

DLLを利用する側のコードを書きます

これも名前はどうでもいいのですが、Source.cppとします。

クライアント側のソースコード


※includeのところで../BmiDll/Bmi.hという相対パスでダサいことになっていますが、
ちゃんと追加のインクルードディレクトリディレクトリを指定すれば
大丈夫です(プロジェクトのプロパティを開いて
構成プロパティ > C/C++ > 全般 > 追加のインクルードディレクトリ
で設定してやれば単にBmiDll.hなどとできます。)
(参考: http://visualstudiostudy.blog.fc2.com/blog-entry-4.html)

DLLを利用する側のプロジェクトをビルドします

例によって下記が出ればOKです。

========== すべてリビルド: 1 正常終了、0 失敗、0 スキップ ==========

スタートアッププロジェクトを設定します

ソリューションエクスプローラーでソリューションの上で右クリックします。
スタートアッププロジェクトの
シングルスタートアッププロジェクトを
BmiTestMainに変えてやります。
f:id:fa11enprince:20140620015305p:plain

実行

ようやく実行できます。ふー疲れた。
デバッグなしで開始します。
コマンドプロンプトに下記が出ればOKです。

一郎さんのBMIは17.13で、低体重です。
二郎さんのBMIは23.22で、普通体重です。
三郎さんのBMIは26.04で、肥満(1度)です。
四郎さんのBMIは30.37で、肥満(2度)です。
五郎さんのBMIは38.33で、肥満(3度)です。
六郎さんのBMIは42.83で、肥満(4度)です。
続行するには何かキーを押してください . . .

(備考)その他 呼び出し規約について

関数呼び出しの内部を理解する(2) – 二流プログラマの三流な日常
ライブラリを作るときに__stdcallを使うことがあると思うが、

__stdcallはWINAPI(Win32 APIの修飾子)でも定義されているもので、他言語からもこの関数を呼び出したい場合は__stdcallにする必要がある。

http://www.ne.jp/asahi/hishidama/home/tech/vcpp/dllusage.html

と書いています。
明示的リンクの詳細なやり方はこちらです。

Visual C++ 2013でDLLを作成して動的読み込みしてみる。 - 小さい頃はエラ呼吸

明示的リンクが適している場合

・他の人が作ったDLLを使わせてもらうのでインポート ライブラリ(この例ではsub.lib)が手に入らない。
・不測の事態でDLLが見つからない場合にドカンとエラーダイアログを出すのではなくて,もうちょっとマイルドな感じのメッセージを出すようにしたい. あるいは 必要な関数をもっていそうな別のDLLを試すなどのエラー回復を試してからでも遅くないと思うとか. たとえば,ある人が作ってくれたabc5.dllというDLLを使う場合, このDLLが去年まではabc4.dllという名前だったというようなことはありうる. もちろん自分のユーザにはabc5.dllにアップデートするよう強制するという方法もないではないけども, 普通に考えれば(DLLがアップデートされた理由が自分が利用している関数でないなら)古いバージョンのabc4.dllが インストールしてあればOKとする方が親切だ. フリーウェアなんかだと せっかくダウンロードしてもらっても,abc5.dllにアップデートしろみたいなメッセージが出ただけで もう めんどくさくなってアンインストールを選択するかもしれない。
・DLLのバージョンを調べて,ある条件を満たす場合だけ実行を継続するというような処理が必要. 前項とは逆のパターンで,同じ名前の古いDLLが使われるのを拒否するためにこういう処理が必要になる場合がある. たとえばバージョン3以前のDLLには重大なバグがあることがわかっているというような場合, こういう防衛線をはっておかないと,ほんとはDLLの問題なのに自分がヘボいプログラムを配布していると誤解されかねない。

ダイナミック リンク ライブラリ(DLL)の基礎知識

また、
VC++DLL作成補足(Hishidama's VC++Memo "DLL")
に暗黙的リンクと明示的リンクとの違い書いてありそれを引用します。

暗黙的リンクと明示的リンクの違い

暗黙的(静的)リンク 明示的(動的)リンク
関数の宣言 DLLの関数を__declspecl(dllimport)を付けて宣言する。(付けなくてもいいらしいが、付けた方が多少効率がいいらしい) DLLの関数を宣言しない。代わりにtypedefしておく(のが便利)。
リンカー リンカーでlibファイルをリンクする必要がある。 リンカーでのリンクは不要。
DLLのロード 実行の準備段階でDLLがロードされる。見つからない場合は実行されない。 LoadLibraryでdllファイルをロードする。見つからない場合はエラーが返る。
関数との紐付け libファイルの中で、dllファイル内で定義されている関数名(や序数)が保持されている。この関数名(あるいは序数)が一致しないと実行時エラーとなり、実行されない。 GetProcAddressで、dllファイル内で定義されている関数名(あるいは序数)を指定する。この関数名(または序数)が一致しないとエラーが返る。
暗黙・明示とは プログラマーが意識しないレベル(実行の準備段階)でDLLが勝手にロードされるので、『暗黙的』。 プログラマーがLoadLibraryを呼び出してやらないとDLLをロードできないので、『明示的』。
静的・動的とは リンカーでリンクした時点で使用するDLLの種類や関数が決まってしまい、変更できないので『静的』。 LoadLibrary・GetProcAddressで、使用するDLLの種類や関数を自由に変更できるので『動的』。