「ライブラリの基礎 - DLLの作成(C/C++/MFC)」の版間の差分

提供:MochiuWiki : SUSE, EC, PCB
ナビゲーションに移動 検索に移動
編集の要約なし
 
(同じ利用者による、間の21版が非表示)
1行目: 1行目:
== 概要 ==
== 概要 ==
MFCを使ったDLLには、以下の2つの種類がある。<br>
MFC DLLには、以下の2つの種類がある。<br>
# 1つ目は"拡張DLL"で、DLLを使用するEXEやDLLもMFCで作成する時にのみ使うことができる。
* 拡張DLL
# 2つ目は"MFCの共有DLLを使用"(Regular DLL)で、これは内部的にMFCそのものを持っているため、DLLを使用するEXEやDLLをMFCで作成しなくても使用できる。
*: DLLを使用するEXEやDLLもMFCで作成する時にのみ使用する。
* MFCの共有DLL(Regular DLL)
*: 内部的にMFCそのものを持っているため、DLLを使用するEXEやDLLをMFCで作成しない時でも使用できる。
<br><br>
<br><br>


== DLLの作成方法(defファイルを作成しない方法) ==
== DLLの作成方法 ==
defファイルを作成しない方法を記載する。<br>
==== 呼び出し規約 ====
[プロジェクト]メニューバー - [プロパティ]を選択して、[プロパティ]ダイアログを表示する。<br>
[プロパティ]ダイアログの[構成のプロパティ] - [C/C++] - [詳細設定]を選択する。<br>
[プロパティ]ダイアログの右ペインから、[呼び出し規約]プルダウンを<u>__cdecl (/Gd)</u>に変更する。 (デフォルト)<br>
<br>
<br>
Visual Studioの[プロジェクト]→[プロパティ]を選択する。<br>
==== モジュール定義ファイル (defファイル) を使用する場合 ====
[C++]→[プリプロセッサの定義]に"DLL"プリプロセッサを追加する。(“Stdafx.h”ファイルに”DLL”プリプロセッサを記載してもよい)<br>
モジュール定義 (.def拡張子) ファイルは、DLLがエクスポートする関数を記述したファイルであり、リンクするプログラムに関するエクスポート、属性、その他の情報をリンカに提供する。<br>
モジュール定義ファイルは、DLLをビルドする場合に最も役立つ。<br>
<br>
<br>
まず、"MainDLL.h"ファイルの先頭に以下のソースコードを追加する。<br>
モジュール定義ステートメントの代わりに使用できるMSVCリンカオプションがあるため、モジュール定義ファイルが存在してくてもDLLを開発することができる。<br>
  <source lang="c++">
モジュール定義ファイルを使用しない場合は、エクスポートされる関数を指定する方法として、<code>__declspec(dllexport)</code>を使用する。<br>
  MainDLL.h // ファイルの先頭に以下を追加する。
<br>
<u>※注意</u><br>
<u>エクスポートを行わない実行ファイルを開発する場合、モジュール定義ファイルを使用すると、出力ファイルのサイズが大きくなり読み込みが遅くなることに注意する。</u><br>
<br>
[ソリューションエクスプローラ]を右クリックして、コンテキストメニューから[追加] - [新しい項目]を選択する。<br>
[新しい項目の追加]ダイアログが開くので、モジュール定義ファイル名 <u><ファイル名>.def</u> を入力する。<br>
[OK]ボタンを押下すると、モジュール定義ファイルが自動的に作成される。<br>
<br>
<span style="color:#C00000"><u>※ C++でDLLを作成する場合は、defファイルを作成することを推奨する。</u></span><br>
<br>
[プロジェクト]メニューバー - [プロパティ]を選択して、[プロパティ]ダイアログを表示する。<br>
[プロパティ]ダイアログの[リンカー] - [入力] - [モジュール定義ファイル]項目に使用するモジュール定義ファイル名を入力する。<br>
[OK]ボタン、または、[適用]ボタンを押下して、変更内容を保存する。<br>
<br>
[[ファイル:MFC DLL 01.png|フレームなし|中央]]
<br>
<u>※注意</u><br>
<u>C#で開発したモジュール(EXEまたはDLL)からC++ DLLを呼び出す場合、C++ DLLではモジュール定義ファイルを使用すること。</u><br>
<br>
モジュール定義ファイルを作成して、以下に示すようにエクスポートする関数を記述する。<br>
また、<u>@1</u>等の序数値の記載は任意である。<br>
<syntaxhighlight lang="ini">
; MainDLL.def
LIBRARY MainDLL
EXPORTS
        SampleFunc  @1
        TestFunc    @2
</syntaxhighlight>
<br>
次に、"MainDLL.h"ファイルを作成する。<br>
モジュール定義ファイルを使用する場合、エクスポートする関数名に<code>extern "C"</code>キーワードおよび<code>DECLSPEC __declspec(dllexport)</code>キーワードを付加する必要はない。<br>
<syntaxhighlight lang="c++">
// MainDLL.h
#pragma once
int    SampleFunc(int *lp1, int *lp2);
double TestFunc(double *lp1, double *lp2);
</syntaxhighlight>
<br>
最後に、"MainDLL.cpp"ファイルには以下のように記載する。<br>
  <syntaxhighlight lang="c++">
  // MainDLL.cpp
#include "Stdafx.h"
#include "MainDLL.h"
int SampleFunc(int *lp1, int *lp2)
{
    // ...処理 1
}
int TestFunc(double *lp1, double *lp2)
{
    // ...処理 2
}
</syntaxhighlight>
<br>
モジュール定義ファイルの詳細を知りたい場合は、[https://learn.microsoft.com/ja-jp/cpp/build/reference/module-definition-dot-def-files?view=msvc-170 Microsoftの公式ドキュメント]を参照すること。<br>
<br>
 
==== モジュール定義ファイル (defファイル) を使用しない場合 ====
モジュール定義ファイルを使用しない場合は、<code>extern "C"</code>キーワードおよび<code>__declspec(dllexport)</code>キーワードを付加する必要がある。<br>
<br>
C言語では同じ関数名を複数定義できないため、関数名の一意性があるが、C++では関数のオーバーロードができるため、同じ関数名を区別するためにDLLの出力時に関数名が自動的に変更される。<br>
エクスポートする関数名の一意に決定するため、<code>extern "C"</code>キーワードを付加することにより、DLLの出力時に特定の関数名を変更しないようにする。<br>
<br>
Visual Studioの[プロジェクト] - [プロパティ]を選択する。<br>
[C++] - [プリプロセッサの定義]に"DLL"プリプロセッサを追加する。(“Stdafx.h”ファイルに”DLL”プリプロセッサを記載してもよい)<br>
<br>
まず、"MainDLL.h"ファイルを作成する。<br>
<syntaxhighlight lang="c++">
// MainDLL.h
   
   
  #ifndef __MAINDLL_H__
  #ifndef __MAINDLL_H__
24行目: 104行目:
  #endif
  #endif
   
   
  // 以下略
  // エクスポートする関数
#endif //__MAINDLL_H__
</source>
<br>
次に、上記の"MainDLL.h"ファイルにエクスポートしたい関数を以下のように記載する。<br>
<source lang="c++">
MainDLL.h
  #ifdef __cplusplus
  #ifdef __cplusplus
     extern "C"
     extern "C"
38行目: 110行目:
  #endif
  #endif
   
   
DLL_EXPORT int WINAPI SampleFunc(CString &p_rcStr, CWnd *p_pcWnd);
      DLL_EXPORT int SampleFunc(CString &p_rcStr, CWnd *p_pcWnd);
   
   
  #ifdef __cplusplus
  #ifdef __cplusplus
     }
     }
  #endif
  #endif
  </source>
#endif //__MAINDLL_H__
  </syntaxhighlight>
<br>
<br>
"MainDLL.cpp"ファイルの先頭に以下を記載する。<br>
次に、"MainDLL.cpp"ファイルを作成する。<br>
  <source lang="c++">
  <syntaxhighlight lang="c++">
  MainDLL.cpp
  // MainDLL.cpp
   
   
  #include "StdAfx.h"
  #include "StdAfx.h"
  #include "MainDLL.h"
  #include "MainDLL.h"
</source>
 
<br>
  extern "C" int SampleFunc(CString &p_rcStr, CWnd *p_pcWnd)
"MainDLL.cpp"ファイルにて関数を作成する時は、以下のように記載する。<br>
<source lang="c++">
MainDLL.cpp
  extern "C" int WINAPI SampleFunc(CString &p_rcStr, CWnd *p_pcWnd)
  {
  {
     // 以下略
     // 以下略
  }
  }
  </source>
  </syntaxhighlight>
<br>
<code>extern "C"</code>キーワードを付加しない場合、他の実行ファイル等からDLLを使用すると、関数が見つからないとエラーが発生する。<br>
ネイティブの実行ファイルから呼び出す場合、コンパイル時にエラーが発生する場合が多い。<br>
ただし、マネージドの実行ファイルからC++ DLLを呼び出す場合、<u>実行時に</u>例外<code>System.EntryPointNotFoundException</code>が発生する。<br>
<br><br>
<br><br>


== DLLの作成方法(defファイルを作成する) ==
== DLLファイルの確認 ==
defファイルを作成する方法を記載する。<br>
DLLファイルのエクスポートされた関数名は、dumpbinで確認することができる。<br>
<br>
<br>
defファイルを作成しない方法では、DLLの関数をエクスポートするために"__declspec"キーワードを使用したが、関数のエクスポートは"defファイル"を利用することでも可能である。
PowerShellまたはコマンドプロンプトを起動して、以下のコマンドを実行する。<br>
"defファイル"によるエクスポートを採用した場合、 エクスポート関数名にキーワードを付ける必要は無い。
この時、name項目に関数名が表示される。<br>
dumpbin.exe /EXPORTS <DLLファイル名>
# 出力
Microsoft (R) COFF/PE Dumper Version 9.00.30729.01
Copyright (C) Microsoft Corporation.  All rights reserved.
Dump of file <DLLファイル名>
File Type: DLL
Section contains the following exports for sample.dll
    00000000 characteristics
    49A74D91 time date stamp Fri Feb 27 01:18:57 2009
        0.00 version
          1 ordinal base
          1 number of functions
          1 number of names
    ordinal hint RVA      name
          1    0 00001034 FunctionName
  Summary
        1000 .data
        1000 .pdata
        1000 .rdata
        1000 .reloc
        1000 .text
<br><br>
 
== DLLの使用 ==
Visual StudioでDLLを作成している場合、プロジェクトから参照に追加することで使用できる。<br>
他のIDE等でDLLを作成している場合、共有ライブラリとして使用するには、コンパイル時に以下の3つが必要となる。<br>
* ヘッダーファイル (.h)
* インポートライブラリ (.lib)
* DLL
<br>
<br>
'''※ C#等で作成したEXEまたはDLLからMFC DLLを呼び出す場合、MFC DLLでは”defファイル”を使用すること'''
また、外部から呼び出せる関数が存在しない場合、インポートライブラリ(.lib)は作成されない。<br>
'''※ defファイルとは、DLLがエクスポートする関数を記述したファイルのことである。'''
<br><br>
'''  [追加]→[新しい項目]を押下して、[新しい項目の追加]ダイアログにて”xxx.def”を追加するとdefファイルが作成される。'''


[プロジェクト]メニューの[プロパティ]を押下して、プロパティダイアログを表示する。
== インポートライブラリの作成 ==
プロパティダイアログの[リンカー]→[入力]の[モジュール定義ファイル]に使用するdefファイルの名前を記述する。
インポートライブラリ(.lib)が提供されていない場合、DLLファイルから作成できる。<br>
[[ファイル:MFC DLL 01.png|フレームなし|中央]]
<br>
まず、dumpbinを実行して、エクスポートされた全ての定義をファイルに出力する。<br>
dumpbin /exports target.dll > exports.txt
<br>
1行目にLIBRARY DLL名、2行目にEXPORTSを記述をしたDEFファイルを作成する。<br>
echo LIBRARY <Dllファイル名> > target.def
echo EXPORTS >> target.def
<br>
DEFファイルに、最初に出力したファイルから関数の定義部分だけを追記する。<br>
for /f "skip=19 tokens=4" %A in (exports.txt) do echo %A >> target.def
<br>
libコマンドを実行して、DEFファイルからインポートライブラリを作成する。<br>
# 32bitを対象とする場合
lib /def:target.def /out:target.lib /machine:x86
# 64bitを対象とする場合
lib /def:target.def /out:target.lib /machine:x64
<br>
<br>
defファイルを作成して、以下のようにエクスポートしたい関数を記載する。
インポートライブラリを参照するには、リンカーのオプションにて、[追加の依存ファイル]で指定する。<br>
※但し、"@1"等の序数値は記載しなくてもよい
これは、/DYNAMICBASEオプションを指定することと同じことである。<br>
または、ソースコード内において、#pragmaディレクティブで指定する。<br>
<br><br>


---defファイル(ここから)---
== DLLの暗黙的リンクと明示的リンクの違い ==
LIBRARY MainDLL
<center>
 
{| class="wikitable" | style="background-color:#fefefe;"
EXPORTS
|-
SampleFunc @1
! style="background-color:#66CCFF;" | 
TestFunc @2
! style="background-color:#66CCFF;" | 暗黙的(静的)リンク
---defファイル(ここまで)---
! style="background-color:#66CCFF;" | 明示的(動的)リンク
 
|-
次に、"MainDLL.h"ファイルは以下のように記載する
| 関数の宣言 || DLLの関数に__declspecl(dllimport)を付けて宣言する。 || DLLの関数を宣言せず、typedefする。
 
|-
---ヘッダファイル(ここから)---
| リンカ || リンカでライブラリファイルをリンクする必要がある。 || リンカでのリンクは不要である。
#pragma once
|-
 
| DLLの読み込み || EXEの実行の準備段階でDLLを読み込む。<br>DLLが見つからない場合、EXEは実行されない。 || <code>LoadLibrary</code>関数でDLLを読み込む。<br>DLLが見つからない場合、NULLが返る。<br>DLL内の関数を実行するには、<code>GetProcAddress</code>関数で関数アドレスを取得する必要がある。
#ifndef _USRDLL
|-
#define DLL_EXPORT extern "C" __declspec(dllimport)
| 関数との紐付け || ライブラリファイルで、DLL内の関数名(序数も含む)が保持されている。<br>この関数名(あるいは序数)が一致しないとNULLが返る。 || <code>GetProcAddress</code>関数で、DLL内の関数名(あるいは序数)を指定する。<br>この関数名(または序数)が一致しないとNULLが返る。
#else
|-
#define DLL_EXPORT
| 暗黙的 / 明示的とは || EXEの実行準備段階でDLLを自動で読み込むので、暗黙的という。 || 設計者が<code>LoadLibrary</code>関数を使用してDLLを呼び出すので、明示的という。
#endif
|-
 
| 静的 / 動的とは || リンカでリンクした時点で使用するDLLの種類や関数が決まるので、静的という。 || <code>LoadLibrary</code>関数と<code>GetProcAddress</code>関数を使用してDLL内の関数を自由に使用できるので、動的という。
DLL_EXPORT int __stdcall SampleFunc(int *lp1, int *lp2);
|}
DLL_EXPORT double __stdcall TestFunc(double *lp1, double *lp2);
</center>
---ヘッダファイル(ここまで)---
<br><br>
 
最後に、"MainDLL.cpp"ファイルには以下のように記載する
 
---cppファイル(ここから)---
#include “Stdafx.h”
#include "MainDLL.h"
 
int __stdcall SampleFunc(int *lp1, int *lp2)
{
// 以下略
}
 
int __stdcall TestFunc(double *lp1, double *lp2)
{
// 以下略
}
---cppファイル(ここまで)---


== DLLの暗黙的リンク ==
EXEファイルのプロジェクトに、DLLファイルを暗黙的リンクする方法を記載する。<br>
<br>
以下の例では、CppEXE.exeとCppDLL.dllが存在するものとして設定している。<br>
# まず、CppEXE.exeのプロジェクトを起動する。
# [プロジェクト]メニュー - [CppEXEのプロパティ] - [構成プロパティ] - [C/C++] - [全般] - [追加のインクルードディレクトリ]項目に、<br>CppDLL.dllのヘッダファイル(CppDLL.h)が存在するディレクトリを追加する。
# 次に、[構成プロパティ] - [リンカー] - [全般] - [追加のライブラリディレクトリ]項目に、<br>DLLのライブラリファイル(CppDLL.lib)が存在するディレクトリを追加する。
# 最後に、[構成プロパティ] - [リンカー] - [入力] - [追加の依存ファイル]項目に、CppDLL.libと記述する。
<br><br>


== DLLの明示的リンク ==
DLLの明示的リンクを行う場合、Windows APIの<code>LoadLibrary</code>関数を使用する。<br>
DLLファイルが見つからない場合は、ハンドルがNULLで返るため、エラーハンドリングを行う。<br>
<br>
以下の例では、Sample.dllファイルに定義されたHello関数を呼んでいる。<br>
<syntaxhighlight lang="c++">
// DLLファイルの読み込み
HMODULE hModule = ::LoadLibrary(_T("Sample.dll"));
if(hModule == NULL)
{
    std::cerr << _T("DLLファイルの読み込みに失敗しました") << std::endl;
    return 0;
}
</syntaxhighlight>
<br>
次に、DLLファイルから呼ぶ関数のアドレスを取得する。<br>
まず、DLLファイルに定義されたHello関数の呼び出すには、Hello関数の関数ポインタを定義する。<br>
<br>
Hello関数のアドレスを取得するには、<code>GetProcAddress</code>関数を使用する。<br>
Hello関数のアドレスの取得に失敗した場合は、必ず<code>FreeLibrary</code>関数を呼び、DLLファイルのハンドルを解放する。<br>
<syntaxhighlight lang="c++">
// C++03以前
// typedef int (*FUNC)(char *);
// C++11以降
using FUNC = int (*)(char *);
// 関数のアドレス取得
FUNC lpFunc = (FUNC)::GetProcAddress(hModule, "Hello");
if (lpFunc == NULL)
{
    std::cerr << _T("関数のアドレス取得に失敗しました") << std::endl;
    ::FreeLibrary(hModule);
    return 0;
}
</syntaxhighlight>
<br>
最後に、GetProcAddress関数で取得したHello関数のアドレスに引数を渡す。<br>
<syntaxhighlight lang="c++">
// 関数の呼び出し
int ret = (*lpFunc)(tmp);
if(ret != 0)
{
    std::cerr << _T("関数の呼び出しに失敗しました") << std::endl;
    ::FreeLibrary(hModule);
    return 0;
}
</syntaxhighlight>
<br>
以下に、サンプルコードの全体を示す。<br>
<syntaxhighlight lang="c++">
#include "stdafx.h"
#include <Windows.h>
#include <conio.h>
// C++03以前
// typedef int (*FUNC)(char *);
// C++11以降
using FUNC = int (*)(char *);
int _tmain(int argc, _TCHAR* argv[])
{
    // DLLを読み込む
    HMODULE hModule = ::LoadLibrary(_T("Sample.dll"));
    if (hModule == NULL)
    {
      std::cerr << _T("DLLファイルの読み込みに失敗しました") << std::endl;
      return 0;
    }
    // Hello関数のアドレスを取得
    FUNC lpFunc = (FUNC)::GetProcAddress(hModule, _T("Hello"));
    if (lpFunc == NULL)
    {
      std::cerr << _T("Hello関数のアドレス取得に失敗しました") << std::endl;
      ::FreeLibrary(hModule);
      return 0;
    }
    // Hello関数の実行
    char tmp[12 + 1];  // Hello関数の引数
    memset(tmp, 0, sizeof(tmp));
    int ret = (*lpFunc)(tmp);
    if (ret != 0)
    {
      std::cerr << _T("Hello関数の呼び出しに失敗しました") << std::endl;
      ::FreeLibrary(hModule);
      return 0;
    }
    // DLLの解放
    ::FreeLibrary(hModule);
    std::cerr << tmp << std::endl;
    _getch();
    return 0;
}
</syntaxhighlight>
<br><br>


== デバッグ方法 ==
C++DLLのデバッグ方法を、以下に記載する。<br>
<br>
# [ソリューションエクスプローラー]において、C++DLLプロジェクトを右クリックして、[プロパティ]を選択する。<br>
# [<Project>プロパティページ]画面が開くので、画面上部の[構成]プルダウンから[デバッグ]を選択する。
# 次に、画面左にある[構成プロパティ] - [デバッグ]を選択する。
# 画面右にある[起動するデバッガー]プルダウンから[ローカルWindowsデバッガー]または[リモートWindowsデバッガー]のいずれかを選択する。
# 画面右にある[コマンド]項目または[リモートコマンド]項目において、呼び出し元の実行ファイルのフルパスを入力する。<br>コマンドライン引数が必要な場合、[コマンド引数]項目に任意の必要なコマンドライン引数を入力する。
# [OK]ボタンを押下する。
<br><br>


__FORCETOC__
__FORCETOC__
[[カテゴリ:MFC]]
[[カテゴリ:C]][[カテゴリ:C++]][[カテゴリ:MFC]]

2024年2月10日 (土) 18:05時点における最新版

概要

MFC DLLには、以下の2つの種類がある。

  • 拡張DLL
    DLLを使用するEXEやDLLもMFCで作成する時にのみ使用する。
  • MFCの共有DLL(Regular DLL)
    内部的にMFCそのものを持っているため、DLLを使用するEXEやDLLをMFCで作成しない時でも使用できる。



DLLの作成方法

呼び出し規約

[プロジェクト]メニューバー - [プロパティ]を選択して、[プロパティ]ダイアログを表示する。
[プロパティ]ダイアログの[構成のプロパティ] - [C/C++] - [詳細設定]を選択する。
[プロパティ]ダイアログの右ペインから、[呼び出し規約]プルダウンを__cdecl (/Gd)に変更する。 (デフォルト)

モジュール定義ファイル (defファイル) を使用する場合

モジュール定義 (.def拡張子) ファイルは、DLLがエクスポートする関数を記述したファイルであり、リンクするプログラムに関するエクスポート、属性、その他の情報をリンカに提供する。
モジュール定義ファイルは、DLLをビルドする場合に最も役立つ。

モジュール定義ステートメントの代わりに使用できるMSVCリンカオプションがあるため、モジュール定義ファイルが存在してくてもDLLを開発することができる。
モジュール定義ファイルを使用しない場合は、エクスポートされる関数を指定する方法として、__declspec(dllexport)を使用する。

※注意
エクスポートを行わない実行ファイルを開発する場合、モジュール定義ファイルを使用すると、出力ファイルのサイズが大きくなり読み込みが遅くなることに注意する。

[ソリューションエクスプローラ]を右クリックして、コンテキストメニューから[追加] - [新しい項目]を選択する。
[新しい項目の追加]ダイアログが開くので、モジュール定義ファイル名 <ファイル名>.def を入力する。
[OK]ボタンを押下すると、モジュール定義ファイルが自動的に作成される。

※ C++でDLLを作成する場合は、defファイルを作成することを推奨する。

[プロジェクト]メニューバー - [プロパティ]を選択して、[プロパティ]ダイアログを表示する。
[プロパティ]ダイアログの[リンカー] - [入力] - [モジュール定義ファイル]項目に使用するモジュール定義ファイル名を入力する。
[OK]ボタン、または、[適用]ボタンを押下して、変更内容を保存する。

MFC DLL 01.png


※注意
C#で開発したモジュール(EXEまたはDLL)からC++ DLLを呼び出す場合、C++ DLLではモジュール定義ファイルを使用すること。

モジュール定義ファイルを作成して、以下に示すようにエクスポートする関数を記述する。
また、@1等の序数値の記載は任意である。

 ; MainDLL.def
 
 LIBRARY MainDLL
 
 EXPORTS
         SampleFunc   @1
         TestFunc     @2


次に、"MainDLL.h"ファイルを作成する。
モジュール定義ファイルを使用する場合、エクスポートする関数名にextern "C"キーワードおよびDECLSPEC __declspec(dllexport)キーワードを付加する必要はない。

 // MainDLL.h
 
 #pragma once
 
 int    SampleFunc(int *lp1, int *lp2);
 double TestFunc(double *lp1, double *lp2);


最後に、"MainDLL.cpp"ファイルには以下のように記載する。

 // MainDLL.cpp
 
 #include "Stdafx.h"
 #include "MainDLL.h"
 
 int SampleFunc(int *lp1, int *lp2)
 {
    // ...処理 1
 }
 
 int TestFunc(double *lp1, double *lp2)
 {
    // ...処理 2
 }


モジュール定義ファイルの詳細を知りたい場合は、Microsoftの公式ドキュメントを参照すること。

モジュール定義ファイル (defファイル) を使用しない場合

モジュール定義ファイルを使用しない場合は、extern "C"キーワードおよび__declspec(dllexport)キーワードを付加する必要がある。

C言語では同じ関数名を複数定義できないため、関数名の一意性があるが、C++では関数のオーバーロードができるため、同じ関数名を区別するためにDLLの出力時に関数名が自動的に変更される。
エクスポートする関数名の一意に決定するため、extern "C"キーワードを付加することにより、DLLの出力時に特定の関数名を変更しないようにする。

Visual Studioの[プロジェクト] - [プロパティ]を選択する。
[C++] - [プリプロセッサの定義]に"DLL"プリプロセッサを追加する。(“Stdafx.h”ファイルに”DLL”プリプロセッサを記載してもよい)

まず、"MainDLL.h"ファイルを作成する。

 // MainDLL.h
 
 #ifndef __MAINDLL_H__
 #define __MAINDLL_H__
 
 #ifdef DLL
    #define DLL_EXPORT	__declspec(dllexport)
 #else
    #define DLL_EXPORT	__declspec(dllimport)
 #endif
 
 // エクスポートする関数
 #ifdef __cplusplus
    extern "C"
    {
 #endif
 
      DLL_EXPORT int SampleFunc(CString &p_rcStr, CWnd *p_pcWnd);
 
 #ifdef __cplusplus
    }
 #endif
 
 #endif	//__MAINDLL_H__


次に、"MainDLL.cpp"ファイルを作成する。

 // MainDLL.cpp
 
 #include "StdAfx.h"
 #include "MainDLL.h"
  
 extern "C" int SampleFunc(CString &p_rcStr, CWnd *p_pcWnd)
 {
    // 以下略
 }


extern "C"キーワードを付加しない場合、他の実行ファイル等からDLLを使用すると、関数が見つからないとエラーが発生する。
ネイティブの実行ファイルから呼び出す場合、コンパイル時にエラーが発生する場合が多い。
ただし、マネージドの実行ファイルからC++ DLLを呼び出す場合、実行時に例外System.EntryPointNotFoundExceptionが発生する。


DLLファイルの確認

DLLファイルのエクスポートされた関数名は、dumpbinで確認することができる。

PowerShellまたはコマンドプロンプトを起動して、以下のコマンドを実行する。
この時、name項目に関数名が表示される。

dumpbin.exe /EXPORTS <DLLファイル名>

# 出力
Microsoft (R) COFF/PE Dumper Version 9.00.30729.01
Copyright (C) Microsoft Corporation.  All rights reserved.

Dump of file <DLLファイル名>

File Type: DLL

Section contains the following exports for sample.dll

   00000000 characteristics
   49A74D91 time date stamp Fri Feb 27 01:18:57 2009
       0.00 version
          1 ordinal base
          1 number of functions
          1 number of names

   ordinal hint RVA      name

         1    0 00001034 FunctionName

 Summary

       1000 .data
       1000 .pdata
       1000 .rdata
       1000 .reloc
       1000 .text



DLLの使用

Visual StudioでDLLを作成している場合、プロジェクトから参照に追加することで使用できる。
他のIDE等でDLLを作成している場合、共有ライブラリとして使用するには、コンパイル時に以下の3つが必要となる。

  • ヘッダーファイル (.h)
  • インポートライブラリ (.lib)
  • DLL


また、外部から呼び出せる関数が存在しない場合、インポートライブラリ(.lib)は作成されない。


インポートライブラリの作成

インポートライブラリ(.lib)が提供されていない場合、DLLファイルから作成できる。

まず、dumpbinを実行して、エクスポートされた全ての定義をファイルに出力する。

dumpbin /exports target.dll > exports.txt


1行目にLIBRARY DLL名、2行目にEXPORTSを記述をしたDEFファイルを作成する。

echo LIBRARY <Dllファイル名> > target.def
echo EXPORTS >> target.def


DEFファイルに、最初に出力したファイルから関数の定義部分だけを追記する。

for /f "skip=19 tokens=4" %A in (exports.txt) do echo %A >> target.def


libコマンドを実行して、DEFファイルからインポートライブラリを作成する。

# 32bitを対象とする場合
lib /def:target.def /out:target.lib /machine:x86

# 64bitを対象とする場合
lib /def:target.def /out:target.lib /machine:x64


インポートライブラリを参照するには、リンカーのオプションにて、[追加の依存ファイル]で指定する。
これは、/DYNAMICBASEオプションを指定することと同じことである。
または、ソースコード内において、#pragmaディレクティブで指定する。


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

暗黙的(静的)リンク 明示的(動的)リンク
関数の宣言 DLLの関数に__declspecl(dllimport)を付けて宣言する。 DLLの関数を宣言せず、typedefする。
リンカ リンカでライブラリファイルをリンクする必要がある。 リンカでのリンクは不要である。
DLLの読み込み EXEの実行の準備段階でDLLを読み込む。
DLLが見つからない場合、EXEは実行されない。
LoadLibrary関数でDLLを読み込む。
DLLが見つからない場合、NULLが返る。
DLL内の関数を実行するには、GetProcAddress関数で関数アドレスを取得する必要がある。
関数との紐付け ライブラリファイルで、DLL内の関数名(序数も含む)が保持されている。
この関数名(あるいは序数)が一致しないとNULLが返る。
GetProcAddress関数で、DLL内の関数名(あるいは序数)を指定する。
この関数名(または序数)が一致しないとNULLが返る。
暗黙的 / 明示的とは EXEの実行準備段階でDLLを自動で読み込むので、暗黙的という。 設計者がLoadLibrary関数を使用してDLLを呼び出すので、明示的という。
静的 / 動的とは リンカでリンクした時点で使用するDLLの種類や関数が決まるので、静的という。 LoadLibrary関数とGetProcAddress関数を使用してDLL内の関数を自由に使用できるので、動的という。



DLLの暗黙的リンク

EXEファイルのプロジェクトに、DLLファイルを暗黙的リンクする方法を記載する。

以下の例では、CppEXE.exeとCppDLL.dllが存在するものとして設定している。

  1. まず、CppEXE.exeのプロジェクトを起動する。
  2. [プロジェクト]メニュー - [CppEXEのプロパティ] - [構成プロパティ] - [C/C++] - [全般] - [追加のインクルードディレクトリ]項目に、
    CppDLL.dllのヘッダファイル(CppDLL.h)が存在するディレクトリを追加する。
  3. 次に、[構成プロパティ] - [リンカー] - [全般] - [追加のライブラリディレクトリ]項目に、
    DLLのライブラリファイル(CppDLL.lib)が存在するディレクトリを追加する。
  4. 最後に、[構成プロパティ] - [リンカー] - [入力] - [追加の依存ファイル]項目に、CppDLL.libと記述する。



DLLの明示的リンク

DLLの明示的リンクを行う場合、Windows APIのLoadLibrary関数を使用する。
DLLファイルが見つからない場合は、ハンドルがNULLで返るため、エラーハンドリングを行う。

以下の例では、Sample.dllファイルに定義されたHello関数を呼んでいる。

 // DLLファイルの読み込み
 HMODULE hModule = ::LoadLibrary(_T("Sample.dll"));
 
 if(hModule == NULL)
 {
    std::cerr << _T("DLLファイルの読み込みに失敗しました") << std::endl;
    return 0;
 }


次に、DLLファイルから呼ぶ関数のアドレスを取得する。
まず、DLLファイルに定義されたHello関数の呼び出すには、Hello関数の関数ポインタを定義する。

Hello関数のアドレスを取得するには、GetProcAddress関数を使用する。
Hello関数のアドレスの取得に失敗した場合は、必ずFreeLibrary関数を呼び、DLLファイルのハンドルを解放する。

 // C++03以前
 // typedef int (*FUNC)(char *);
 
 // C++11以降
 using FUNC = int (*)(char *);
 
 // 関数のアドレス取得
 FUNC lpFunc = (FUNC)::GetProcAddress(hModule, "Hello");
 if (lpFunc == NULL)
 {
    std::cerr << _T("関数のアドレス取得に失敗しました") << std::endl;
    ::FreeLibrary(hModule);
 
    return 0;
 }


最後に、GetProcAddress関数で取得したHello関数のアドレスに引数を渡す。

 // 関数の呼び出し
 int ret = (*lpFunc)(tmp);
 if(ret != 0)
 {
    std::cerr << _T("関数の呼び出しに失敗しました") << std::endl;
    ::FreeLibrary(hModule);
 
    return 0;
 }


以下に、サンプルコードの全体を示す。

 #include "stdafx.h"
 #include <Windows.h>
 #include <conio.h>
 
 // C++03以前
 // typedef int (*FUNC)(char *);
 
 // C++11以降
 using FUNC = int (*)(char *);
 
 int _tmain(int argc, _TCHAR* argv[])
 {
    // DLLを読み込む
    HMODULE hModule = ::LoadLibrary(_T("Sample.dll"));
    if (hModule == NULL)
    {
       std::cerr << _T("DLLファイルの読み込みに失敗しました") << std::endl;
 
       return 0;
    }
 
    // Hello関数のアドレスを取得
    FUNC lpFunc = (FUNC)::GetProcAddress(hModule, _T("Hello"));
    if (lpFunc == NULL)
    {
       std::cerr << _T("Hello関数のアドレス取得に失敗しました") << std::endl;
       ::FreeLibrary(hModule);
 
       return 0;
    }
 
    // Hello関数の実行
    char tmp[12 + 1];  // Hello関数の引数
    memset(tmp, 0, sizeof(tmp));
 
    int ret = (*lpFunc)(tmp);
    if (ret != 0)
    {
       std::cerr << _T("Hello関数の呼び出しに失敗しました") << std::endl;
       ::FreeLibrary(hModule);
 
       return 0;
    }
 
    // DLLの解放
    ::FreeLibrary(hModule);
 
    std::cerr << tmp << std::endl;
    _getch();
 
    return 0;
 }



デバッグ方法

C++DLLのデバッグ方法を、以下に記載する。

  1. [ソリューションエクスプローラー]において、C++DLLプロジェクトを右クリックして、[プロパティ]を選択する。
  2. [<Project>プロパティページ]画面が開くので、画面上部の[構成]プルダウンから[デバッグ]を選択する。
  3. 次に、画面左にある[構成プロパティ] - [デバッグ]を選択する。
  4. 画面右にある[起動するデバッガー]プルダウンから[ローカルWindowsデバッガー]または[リモートWindowsデバッガー]のいずれかを選択する。
  5. 画面右にある[コマンド]項目または[リモートコマンド]項目において、呼び出し元の実行ファイルのフルパスを入力する。
    コマンドライン引数が必要な場合、[コマンド引数]項目に任意の必要なコマンドライン引数を入力する。
  6. [OK]ボタンを押下する。