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

編集の要約なし
 
(同じ利用者による、間の13版が非表示)
8行目: 8行目:


== DLLの作成方法 ==
== DLLの作成方法 ==
==== defファイルを使用する ====
==== 呼び出し規約 ====
defファイルを作成する方法を記載する。<br>
[プロジェクト]メニューバー - [プロパティ]を選択して、[プロパティ]ダイアログを表示する。<br>
[プロパティ]ダイアログの[構成のプロパティ] - [C/C++] - [詳細設定]を選択する。<br>
[プロパティ]ダイアログの右ペインから、[呼び出し規約]プルダウンを<u>__cdecl (/Gd)</u>に変更する。 (デフォルト)<br>
<br>
<br>
defファイルとは、DLLがエクスポートする関数を記述したファイルのことである。<br>
==== モジュール定義ファイル (defファイル) を使用する場合 ====
[ソリューションエクスプローラ]を右クリック - [追加] - [新しい項目]を選択して、[新しい項目の追加]ダイアログにて"<ファイル名>.def"を追加すると、defファイルが作成される。<br>
モジュール定義 (.def拡張子) ファイルは、DLLがエクスポートする関数を記述したファイルであり、リンクするプログラムに関するエクスポート、属性、その他の情報をリンカに提供する。<br>
モジュール定義ファイルは、DLLをビルドする場合に最も役立つ。<br>
<br>
<br>
<span style="color:#C00000"><u>※ C++でDLLを作成する場合は、defファイルを作成する方法を強く推奨する。</u></span><br>
モジュール定義ステートメントの代わりに使用できるMSVCリンカオプションがあるため、モジュール定義ファイルが存在してくてもDLLを開発することができる。<br>
モジュール定義ファイルを使用しない場合は、エクスポートされる関数を指定する方法として、<code>__declspec(dllexport)</code>を使用する。<br>
<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>
[プロジェクト]メニューバー - [プロパティ]を選択して、[プロパティ]ダイアログを表示する。<br>
[プロパティ]ダイアログの[リンカー] - [入力] - [モジュール定義ファイル]項目に使用するdefファイル名を記述する。<br>
[プロパティ]ダイアログの[リンカー] - [入力] - [モジュール定義ファイル]項目に使用するモジュール定義ファイル名を入力する。<br>
[OK]ボタン、または、[適用]ボタンを押下して、変更内容を保存する。<br>
<br>
[[ファイル:MFC DLL 01.png|フレームなし|中央]]
[[ファイル:MFC DLL 01.png|フレームなし|中央]]
<br>
<br>
<u>※注意</u><br>
<u>※注意</u><br>
<u>C#にて作成したモジュール(EXEまたはDLL)からC++ DLLを呼び出す場合、C++ DLLではdefファイルを使用すること。</u><br>
<u>C#で開発したモジュール(EXEまたはDLL)からC++ DLLを呼び出す場合、C++ DLLではモジュール定義ファイルを使用すること。</u><br>
<br>
<br>
defファイルを作成して、以下のようにエクスポートする関数を記載する。<br>
モジュール定義ファイルを作成して、以下に示すようにエクスポートする関数を記述する。<br>
'''※但し、"@1"等の序数値は記載しなくてもよい'''。<br>
また、<u>@1</u>等の序数値の記載は任意である。<br>
  <source lang="c++">
  <syntaxhighlight lang="ini">
  MainDLL.def
  ; MainDLL.def
  LIBRARY MainDLL
  LIBRARY MainDLL
   
   
32行目: 48行目:
         SampleFunc  @1
         SampleFunc  @1
         TestFunc    @2
         TestFunc    @2
  </source>
  </syntaxhighlight>
<br>
<br>
次に、"MainDLL.h"ファイルは以下のように記載する。<br>
次に、"MainDLL.h"ファイルを作成する。<br>
defファイルによるエクスポートを採用した場合、 エクスポート関数名にキーワードを付ける必要は無い。<br>
モジュール定義ファイルを使用する場合、エクスポートする関数名に<code>extern "C"</code>キーワードおよび<code>DECLSPEC __declspec(dllexport)</code>キーワードを付加する必要はない。<br>
  <source lang="c++">
  <syntaxhighlight lang="c++">
  MainDLL.h
  // MainDLL.h
   
   
  #pragma once
  #pragma once
   
   
  #ifndef  _USRDLL
  int   SampleFunc(int *lp1, int *lp2);
#define  DLL_EXPORT  extern "C" __declspec(dllimport)
  double TestFunc(double *lp1, double *lp2);
#else
  </syntaxhighlight>
#define  DLL_EXPORT
#endif
DLL_EXPORT int     __stdcall SampleFunc(int *lp1, int *lp2);
  DLL_EXPORT double   __stdcall TestFunc(double *lp1, double *lp2);
  </source>
<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"
   
   
  int __stdcall SampleFunc(int *lp1, int *lp2)
  int SampleFunc(int *lp1, int *lp2)
  {
  {
     // 以下略
     // ...処理 1
  }
  }
   
   
  int __stdcall TestFunc(double *lp1, double *lp2)
  int TestFunc(double *lp1, double *lp2)
  {
  {
     // 以下略
     // ...処理 2
  }
  }
  </source>
  </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>
<br>
==== defファイルを使用しない ====
C言語では同じ関数名を複数定義できないため、関数名の一意性があるが、C++では関数のオーバーロードができるため、同じ関数名を区別するためにDLLの出力時に関数名が自動的に変更される。<br>
defファイルを作成しない方法を記載する。<br>
エクスポートする関数名の一意に決定するため、<code>extern "C"</code>キーワードを付加することにより、DLLの出力時に特定の関数名を変更しないようにする。<br>
<br>
<br>
Visual Studioの[プロジェクト][プロパティ]を選択する。<br>
Visual Studioの[プロジェクト] - [プロパティ]を選択する。<br>
[C++][プリプロセッサの定義]に"DLL"プリプロセッサを追加する。(“Stdafx.h”ファイルに”DLL”プリプロセッサを記載してもよい)<br>
[C++] - [プリプロセッサの定義]に"DLL"プリプロセッサを追加する。(“Stdafx.h”ファイルに”DLL”プリプロセッサを記載してもよい)<br>
<br>
<br>
まず、"MainDLL.h"ファイルの先頭に以下のソースコードを追加する。<br>
まず、"MainDLL.h"ファイルを作成する。<br>
  <source lang="c++">
  <syntaxhighlight lang="c++">
  MainDLL.h // ファイルの先頭に以下を追加する。
  // MainDLL.h
   
   
  #ifndef __MAINDLL_H__
  #ifndef __MAINDLL_H__
88行目: 104行目:
  #endif
  #endif
   
   
  // 以下略
  // エクスポートする関数
#endif //__MAINDLL_H__
</source>
<br>
次に、上記の"MainDLL.h"ファイルにエクスポートしたい関数を以下のように記載する。<br>
<source lang="c++">
MainDLL.h
  #ifdef __cplusplus
  #ifdef __cplusplus
     extern "C"
     extern "C"
102行目: 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>
 
== DLLファイルの確認 ==
DLLファイルのエクスポートされた関数名は、dumpbinで確認することができる。<br>
<br>
PowerShellまたはコマンドプロンプトを起動して、以下のコマンドを実行する。<br>
この時、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>
<br><br>


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


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


== dumpbin ==
== DLLの暗黙的リンク ==
DLLファイルのエクスポートされた関数名は、dumpbinで確認することができる。<br>
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>
PowerShellまたはコマンドプロンプトを起動して、以下のコマンドを実行する。<br>
以下に、サンプルコードの全体を示す。<br>
この時、name項目に関数名が表示される。<br>
<syntaxhighlight lang="c++">
  dumpbin.exe /EXPORTS <DLLファイル名>
#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;
Microsoft (R) COFF/PE Dumper Version 9.00.30729.01
    }
Copyright (C) Microsoft Corporation.  All rights reserved.
   
   
Dump of file <DLLファイル名>
    // Hello関数のアドレスを取得
    FUNC lpFunc = (FUNC)::GetProcAddress(hModule, _T("Hello"));
    if (lpFunc == NULL)
    {
      std::cerr << _T("Hello関数のアドレス取得に失敗しました") << std::endl;
      ::FreeLibrary(hModule);
   
   
File Type: DLL
      return 0;
    }
   
   
  Section contains the following exports for sample.dll
    // Hello関数の実行
    char tmp[12 + 1]; // Hello関数の引数
    memset(tmp, 0, sizeof(tmp));
   
   
     00000000 characteristics
     int ret = (*lpFunc)(tmp);
     49A74D91 time date stamp Fri Feb 27 01:18:57 2009
     if (ret != 0)
        0.00 version
    {
          1 ordinal base
      std::cerr << _T("Hello関数の呼び出しに失敗しました") << std::endl;
          1 number of functions
      ::FreeLibrary(hModule);
          1 number of names
   
   
     ordinal hint RVA      name
      return 0;
     }
   
   
          1    0 00001034 FunctionName
    // DLLの解放
    ::FreeLibrary(hModule);
   
   
  Summary
    std::cerr << tmp << std::endl;
    _getch();
   
   
        1000 .data
    return 0;
        1000 .pdata
}
        1000 .rdata
</syntaxhighlight>
        1000 .reloc
<br><br>
        1000 .text
 
== デバッグ方法 ==
C++DLLのデバッグ方法を、以下に記載する。<br>
<br>
# [ソリューションエクスプローラー]において、C++DLLプロジェクトを右クリックして、[プロパティ]を選択する。<br>
# [<Project>プロパティページ]画面が開くので、画面上部の[構成]プルダウンから[デバッグ]を選択する。
# 次に、画面左にある[構成プロパティ] - [デバッグ]を選択する。
# 画面右にある[起動するデバッガー]プルダウンから[ローカルWindowsデバッガー]または[リモートWindowsデバッガー]のいずれかを選択する。
# 画面右にある[コマンド]項目または[リモートコマンド]項目において、呼び出し元の実行ファイルのフルパスを入力する。<br>コマンドライン引数が必要な場合、[コマンド引数]項目に任意の必要なコマンドライン引数を入力する。
# [OK]ボタンを押下する。
<br><br>
<br><br>


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