Skip to content

MS_DLLCreationSteps

nishi_74322014 edited this page Aug 21, 2026 · 1 revision

DLL作成手順

  • 戻る(DLL(DLL))

概要

# 関数エクスポート方式 \ DLL呼出方法 暗黙のうちにリンク(暗黙のリンク) 明示的にリンク(明示的なリンク)
1 明示的ロードが不要だが、関数のエクスポートが手間な方式 以下で説明(VC からの DLL 呼び出し)
2 明示的ロードが必要だが、関数のエクスポートが簡単な方式 以下で説明(.NET からの DLL 呼び出し)

補足(この表が本ページの設計図): 一見分かりにくいが、
2 つの独立した軸を組み合わせた表である。

【軸 1:DLL 側 ── どうやって関数を公開するか】
   ① .def ファイル + ヘッダ + __declspec(dllimport/dllexport)
        → 手間だが、名前が【綺麗にエクスポートされる】★
   ② __declspec(dllexport) だけ
        → 簡単だが、C++ の【名前修飾(マングリング)】が残りうる

【軸 2:呼ぶ側 ── どうやって DLL を読み込むか】
   ① 暗黙のリンク(静的) … .lib を使う。起動時にロード
   ② 明示的なリンク(動的) … LoadLibrary / GetProcAddress

なぜこの組み合わせになるのか:

・【VC++ から呼ぶ】場合
    .lib が使える(同じツールチェーン)
      → 暗黙のリンクが自然
      → ただし .lib を作るには .def や dllexport の整備が要る ★

・【.NET から呼ぶ】場合
    .lib を使えない(C# にリンカがない)
      → DllImport(P/Invoke)=【実質的に明示的なリンク】
      → CLR が LoadLibrary / GetProcAddress を代行する
      → DLL 側は dllexport だけあればよい= 簡単

つまり、「呼ぶ側が何か」で DLL 側の作り方が決まる——
というのが表の含意である。

手順

プロジェクトの作成

  • 新規作成→ Win32 プロジェクトを選択
  • ウィザードで、DLL を選択。
  • プロジェクトが作成される。
    • dllmain.cpp は DLL のエントリポイント。
    • (プロジェクト名).cpp にエクスポート関数を定義する。

移行メモ(現在の Visual Studio でのテンプレート名): 「Win32 プロジェクト」
という名称は VS 2015 以前のもので、
現在は以下になっている。

【Visual Studio 2022】
   [新しいプロジェクトの作成]
     → 言語:C++、プラットフォーム:Windows
     → 【ダイナミック リンク ライブラリ (DLL)】★
     → または「Windows デスクトップ ウィザード」で
       アプリケーションの種類に DLL を選ぶ

dllmain.cppDllMain)についての重要な注意:

【DllMain でやってはいけないこと】★
   ・他の DLL の関数を呼ぶ(LoadLibrary、CoInitialize 等)
   ・同期処理(ロック取得、スレッド待ち)
   ・マネージド コードの呼び出し

【理由】
   DllMain は【ローダー ロックを保持したまま】呼ばれる
     → 別の DLL をロードしようとすると【デッドロック】する
     → 「Loader Lock」問題として知られる

【原則】
   DllMain には【何も書かない】のが最も安全
   初期化が必要なら、【明示的な Init 関数】を用意して
   呼び出し側から呼ばせる

エクスポート関数の実装

以下、2つのメソッドをエクスポートしている。

  • Test_MYLIBAPI
  • TestArrayMethod

明示的ロードが不要だが、関数のエクスポートが手間な方式

  • ヘッダ・ファイル
  • ソース・ファイル
  • モジュール定義ファイル

を作成して関数をエクスポートする方式

  • ヘッダ・ファイル(MYLIBAPI.h)
    プロジェクトに、ヘッダ・ファイルを追加する。
// エクスポートとインポートの切り替え
#ifdef MYLIBAPI // VC_DLL_EXPORTS でもいいかも

// DLLのPJではソース・ファイル(MYLIBAPI.cpp)を使用して関数をエクスポート。

#else

// EXEの(DLLを利用する)PJではヘッダ・ファイル(MYLIBAPI.h)を使用して関数をインポート。
// extern "C" は、他の言語から呼ぶ場合に必要。
#define MYLIBAPI extern "C" __declspec(dllimport)

#endif

// 以下、エクスポート関数のプロトタイプ宣言
// 通常、__stdcallを適用する(__stdcall = WINAPI)。
MYLIBAPI void __stdcall Test_MYLIBAPI(LPCWSTR lpText, LPCWSTR lpCaption);

// この方式だと、C4603、C4273の警告が出るが、問題はない。
// ・C4603:'<identifier>': マクロが定義されていないか、
//          プリコンパイル済みヘッダーが使用している定義とは異なります。
// ・C4273:dll リンクが一貫していません。
  • ソース・ファイル(MYLIBAPI.cpp)
    プロジェクトに、ソース・ファイルを追加する。
// このソースコードの関数と変数をエクスポート
#define MYLIBAPI extern "C" __declspec(dllexport)

#include <stdafx.h>
#include <windows.h>
#include <stdio.h>

// エクスポート関数の宣言
#include "MYLIBAPI.h"

// 以下、エクスポート関数の実装
// 通常、__stdcallを適用する(__stdcall = WINAPI)。
void __stdcall Test_MYLIBAPI(LPCWSTR lpText, LPCWSTR lpCaption)
{
	// MessageBoxを呼び出すだけ。
	MessageBox(NULL, lpText, lpCaption, MB_OK);
}
  • モジュール定義ファイル(Exports.def)
    プロジェクトに、モジュール定義ファイルを追加する。
;MS系以外のツールから呼ぶときは、
;DefファイルのEXPORTSを使用してExportする。
LIBRARY	"VC_DLL"
EXPORTS
   Test_MYLIBAPI
   TestArrayMethod

補足(このイディオムの意味と、警告が出る理由): MYLIBAPI マクロの
使い方は Win32 DLL の古典的な定石だが、
原文のコードは意図的に少し崩しているため、整理しておく。

// 【本来の定石】マクロ名と判定に使う識別子を分ける
#ifdef VC_DLL_EXPORTS                    // ← ビルド時に定義(DLL 側)
#  define MYLIBAPI extern "C" __declspec(dllexport)
#else
#  define MYLIBAPI extern "C" __declspec(dllimport)
#endif

MYLIBAPI void __stdcall Test_MYLIBAPI(LPCWSTR lpText, LPCWSTR lpCaption);
【1 つのヘッダを両側で使い回すのが目的】★
   DLL 側   → dllexport になる(公開する)
   利用側   → dllimport になる(取り込む)
     → 宣言を二重管理しなくて済む

原文が警告(C4603 / C4273)を承知の上で使っているのは、
マクロ名そのもの(MYLIBAPI)を #ifdef の判定に使っているためである。
原文のコメント「VC_DLL_EXPORTS でもいいかも」が、
まさに上記の定石を指している。

extern "C" が必要な理由(原文の補足節にもあるが重要):

【C++ の名前修飾(Name Mangling)】
   C++ はオーバーロードを許すため、
   関数名に【引数の型情報を埋め込む】

     void Foo(int)   → ?Foo@@YAXH@Z    のような名前になる ★

【extern "C" を付けると】
     void Foo(int)   → _Foo@4(__stdcall の場合)
     → 【他言語から名前で引ける】
     → C#、VB、Delphi、Python(ctypes) 等から呼べる

.def ファイルの本当の価値:

【extern "C" + __stdcall でも、まだ装飾が残る】
   _Test_MYLIBAPI@8        ← 先頭の _ と @引数バイト数

【.def の EXPORTS に書くと】
   Test_MYLIBAPI           ← 【装飾なしの綺麗な名前】★

 → 原文のコメント「MS系以外のツールから呼ぶときは」が
   まさにこれを指している
 → C# の DllImport でも EntryPoint 指定が不要になる

__stdcall__cdecl(呼び出し規約):

__stdcall(WINAPI) __cdecl
スタックの後始末 呼ばれた側 呼んだ側
可変長引数 不可 (printf 等)
Win32 API こちら
C/C++ の既定 こちら
【最頻出の不具合】★
   DLL 側が __cdecl、C# 側が既定(Winapi=__stdcall)
     → 【スタックが壊れる】
     → 「呼び出し規約が一致しません」または
       すぐには落ちず、後で謎の破損として現れる

【対策】 CallingConvention を明示する
   [DllImport("VC_DLL.dll", CallingConvention = CallingConvention.Cdecl)]

移行メモ(現在の書き方): stdafx.h
VS 2017 以降 pch.h に変わった。
また、#include <stdafx.h>#include "stdafx.h"
(山括弧ではなく引用符)が正しい。

明示的ロードが必要だが、関数のエクスポートが簡単な方式

簡単な方式では明示的ロードで VC、DOTNET から使用するので、
ヘッダ・ファイル、モジュール定義ファイルなどのモジュールが不要。

  • ソース・ファイル(Simple.cpp)
    プロジェクトに、ソース・ファイルを追加する。
// 簡単な方式(明示的ロードが必要)
// Simple.cpp : DLL アプリケーション用にエクスポートされる関数を定義します。

#include "stdafx.h"
#include <stdio.h>

// 以下は、
// The try, catch, and throw Statements
// http://msdn.microsoft.com/en-us/library/6dekhbbc(VS.80).aspx
// から引用

class CTest {
public:
   CTest() {};
   ~CTest() {};
   const char *ShowReason() const { 
      return "Exception in CTest class."; 
   }
};

extern "C"{
	typedef struct _A{
		int m1;
		char m2_in[12]; // 11文字まで (MarshalAs(UnmanagedType.ByValTStr, SizeConst=12)
	 	char m3_out[48]; // 47文字まで (MarshalAs(UnmanagedType.ByValTStr, SizeConst=48)
	} A;

	// 明示的ロードでVC、DOTNETから使用するのでヘッダーファイルは不要
	// なお、__cdeclでもDOTNETから正しく呼び出せることを確認している。
	// 通常、__stdcallを適用する(__stdcall = WINAPI)。
	__declspec(dllexport) int __stdcall TestArrayMethod(A a, A *pa, int len, A aa[]);

	int __stdcall TestArrayMethod(A a, A *pa, int len, A aa[]){

		// 配列の長さが0未満はありえないので
		// 例外を発行する(C++の例外を発行してみる)
		if(len < 0){
			throw CTest();
		} // extern "c" で警告が表示される(__cdeclでも__stdcallでも)。

		// 値渡しの構造体 a
		char *p_inputmessage = a.m2_in;
		int counter = a.m1;

		// 参照渡しの構造体 *pa(構造体 a からコピー)
		pa->m1 = ++counter; // 先に ++ してから代入
		sprintf_s(pa->m3_out, 47, "%s by pa->", p_inputmessage);

		// 参照渡しの構造体配列 aa[](構造体 a からコピー)
		int i;
		for (i = 0; i < len; i++){
			aa[i].m1 = ++counter; // 先に ++ してから代入
			sprintf_s(aa[i].m3_out, 47, "%s by aa[%d].", p_inputmessage, i);
		}

		return counter;
	}
}

補足(このサンプルは相互運用の要点を凝縮している): 構造体の
値渡し・参照渡し・配列渡しを 1 つの関数で試している点が有用である。
C# 側の対応する宣言を明示しておく。

[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Ansi)]
struct A
{
    public int m1;
    [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 12)] public string m2_in;
    [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 48)] public string m3_out;
}
【対応関係】
   C++                   C#
   A a          (値)   A a
   A *pa        (参照) ref A pa
   A aa[]       (配列) [In, Out] A[] aa      ← 原文のコメントの通り ★

UnmanagedType.ByValTStr の注意点:

・SizeConst は【終端の \0 を含む】バイト数
   char m2_in[12] → SizeConst = 12 → 実質 11 文字
   → 原文のコメント「11文字まで」は正しい ★

・CharSet.Ansi なら char[]、CharSet.Unicode なら wchar_t[]
   → 【日本語は Ansi だと 1 文字 2 バイト消費する】
   → CP932 で変換されるため、[エンコーディング](MS_Encoding) の問題が絡む
   → 【新規なら Unicode(wchar_t)にすべき】

throw CTest() の重大な問題(原文も警告に触れている):

【C++ の例外を DLL 境界の外へ投げてはならない】★

   ・extern "C" の関数から C++ 例外を投げる
      → 原文が記録している通り、コンパイラが警告を出す
   ・呼び出し側が C++ でなければ【受け取れない】
      → C# 側では SEHException になる(型情報は失われる)
   ・コンパイラ/ランタイムのバージョンが違うと
      【動作が未定義】

【正しい設計】
   ・DLL の境界では【戻り値でエラーを返す】(HRESULT / int)
   ・詳細なメッセージは out パラメータで返す
   ・DLL 内部で必ず catch する(catch(...) で握る)
// 推奨される形
extern "C" __declspec(dllexport)
int __stdcall TestArrayMethod(A a, A* pa, int len, A aa[])
{
    try
    {
        if (len < 0) return -1;      // ← エラーコードで返す ★
        /* ... */
        return counter;
    }
    catch (...)
    {
        return -99;                  // 例外を境界の外へ出さない
    }
}

DLLの呼び出し方法

  • 参考:Advanced Windows
    • 第 4 部 DLL
      • 第 19 章 DLL の基礎
      • 第 20 章 DLL の高度なテクニック

暗黙のうちにリンク(暗黙のリンク)

*.lib ファイルを使用する。

リンカが *.lib ファイルを使ってインポートされている関数/変数の参照を解決しながら
*.obj モジュールを結合し、
*.exe ファイル(必要な DLL とインポートされているシンボルのリストである
インポートテーブルを格納している)を生成する。

明示的にリンク(明示的なリンク)

明示的なリンクでは、*.lib ファイルを使用せず
(従って、*.exe ファイルはインポートテーブルを持たない)、
Win32API の LoadLibrary を使用して DLL をロードする。

補足(2 方式の実務的な違い): 説明は正確なので、
選択の判断材料を補っておく。

暗黙のリンク 明示的なリンク
ロードのタイミング 起動時(自動) 任意のタイミング
DLL がない場合 起動できない(即エラー)★ 実行時に判定できる
コードの記述量 少ない(呼ぶだけ) 多い(関数ポインタの取得)
起動時間 遅くなる(全 DLL をロード) 速い(必要時のみ)
バージョン違いへの対応 困難 可能(関数の有無を調べられる)
プラグイン機構 不可 これが必須
// 明示的なリンクの典型
HMODULE h = LoadLibrary(L"VC_DLL.dll");
if (h) {
    typedef void (__stdcall *PFN)(LPCWSTR, LPCWSTR);
    auto fn = (PFN)GetProcAddress(h, "Test_MYLIBAPI");
    if (fn) fn(L"text", L"caption");
    FreeLibrary(h);
}
【暗黙のリンクの落とし穴】★
   DLL が見つからないと、【エラー メッセージが不親切】
     「アプリケーションを正しく起動できませんでした (0xc000007b)」
     → 実際は 32bit/64bit の不一致、依存 DLL の欠落 等

   【調査方法】
     ・Dependencies(旧 Dependency Walker の後継)で依存を見る
     ・Process Monitor で DLL の検索経路を追う ★

DLL の検索順序(セキュリティ上も重要):

① アプリケーションのディレクトリ
② システム ディレクトリ(System32)
③ 16 ビット システム ディレクトリ
④ Windows ディレクトリ
⑤ カレント ディレクトリ ★ ← ここが危険
⑥ PATH 環境変数のディレクトリ

【DLL 事前ロード攻撃(DLL Hijacking)】
   ⑤ に悪意ある同名 DLL を置かれると読み込まれてしまう

【対策】
   ・SetDefaultDllDirectories / LOAD_LIBRARY_SEARCH_* を使う
   ・LoadLibrary に【絶対パス】を渡す
   ・SetDllDirectory("") でカレント ディレクトリを外す

DLL呼び出し側サンプル

VC++

  • VC++ からの DLL 呼び出しでは、*.lib ファイルを使用した暗黙のリンクを使用している。
  • *.lib ファイルを参照する場合は、以下の手順に従う。
// VC_Client1.cpp : コンソール アプリケーションのエントリ ポイントを定義します。
//

#include "stdafx.h"
#include <windows.h>
#include "../../VC_DLL/MYLIBAPI.h"

int _tmain(int argc, _TCHAR* argv[])
{
  // 通常のLibを使用したDLL関数の呼び出し方法。
  Test_MYLIBAPI(L"text", L"caption");

  return 0;
}

C#

.NET からの DLL 呼び出し(P/Invoke)では、
明示的なリンクを使用している。

/// <summary>VC_DLL.TestArrayMethod</summary>
[DllImport("VC_DLL.dll")]
private extern static int TestArrayMethod(A a, ref A pa, int len, [In, Out] A[] aa);
// 第4引数(構造体配列)を、結果受け取り用で宣言する場合は 
// "ref" では不可、"[Out]" 属性を設定する必要がある

/// <summary>DLL呼び出し</summary>
private void Button1_Click(object sender, RoutedEventArgs e)
{
  try
  {
    A a1 = new A();
    A a2 = new A();
    A[] a3 = new A[3];

    a1.m1 = 8;
    a1.m2_in = "{入力値側}";

    // 正常系: 第3引数(配列サイズ)を正しく指定
    int ret1 = TestArrayMethod(a1, ref a2, a3.Length, a3);
  }
  catch (SEHException ex)
  {
    //MessageBox
    MessageBox.Show(
      "Err.Number:" + ex.ErrorCode + "\r\n" +
      "Err.Description:" + ex.Message,
      "例外", MessageBoxButton.OK);
  }
}

補足(P/Invoke で必ず確認すべき点): 原文のサンプルは要点を押さえているが、
実務で追加すべき指定を挙げておく。

[DllImport("VC_DLL.dll",
    CallingConvention = CallingConvention.StdCall,  // ← 呼び出し規約を明示 ★
    CharSet = CharSet.Ansi,                          // ← 文字セットを明示 ★
    EntryPoint = "TestArrayMethod",                  // ← 装飾名が違う場合
    SetLastError = false,                            // GetLastError を使うなら true
    ExactSpelling = true)]                           // A/W の自動付与を抑止
private extern static int TestArrayMethod(A a, ref A pa, int len, [In, Out] A[] aa);

[In, Out] の意味(原文のコメントの補足):

【マーシャリングの方向】
   [In]      … マネージド → アンマネージド のみコピー
   [Out]     … アンマネージド → マネージド のみコピー
   [In, Out] … 【双方向】★

【配列で ref が使えない理由】
   配列は既に参照型であり、
   ref A[] は「配列そのものを差し替える」意味になる
     → 中身を書き換えてもらいたい場合は [Out] または [In, Out]
     → 原文のコメントは正しい

SEHException を捕まえている点も適切である。

【ネイティブ側の例外・アクセス違反】
   → CLR では SEHException / AccessViolationException になる

【重要な注意】★
   .NET 4 以降、AccessViolationException は
   【既定で catch できない】(プロセスが落ちる)
     → 破損状態例外(Corrupted State Exception)として扱われる
     → 無理に握ると、壊れた状態のまま動き続けて悪化する

   どうしても必要なら
     [HandleProcessCorruptedStateExceptions]
     + <legacyCorruptedStateExceptionsPolicy enabled="true"/>
   だが、【根本的には DLL 側を直すべき】

32bit / 64bit の一致が最頻出の障害である。

【症状】 BadImageFormatException
【原因】 DLL が 32bit、呼び出し側が 64bit(またはその逆)
【確認】 dumpbin /headers VC_DLL.dll
【対策】 ・DLL を両方ビルドし、実行時に切り替える
         ・または呼び出し側のプラットフォームを固定する
           (AnyCPU + Prefer 32-bit ではなく、x64 を明示)★

現在の代替(LibraryImport:

// .NET 7 以降:Source Generator による P/Invoke ★
[LibraryImport("VC_DLL.dll", StringMarshalling = StringMarshalling.Utf16)]
[UnmanagedCallConv(CallConvs = new[] { typeof(CallConvStdcall) })]
private static partial int TestArrayMethod(A a, ref A pa, int len, [In, Out] A[] aa);
【LibraryImport の利点】
   ・マーシャリング コードが【コンパイル時に生成される】
   ・【Native AOT / トリミングに対応】★
      → DllImport はリフレクションを使うため AOT で問題になる
   ・生成コードを読めるので、挙動が追える

参考

DLLの作成

補足

サンプル

https://github.com/OpenTouryoProject/SampleProgram

  • VC++ DLL:「\InteropWithUnmanage\VC\VC_DLL」
  • VC++ 呼出元:「\InteropWithUnmanage\VC\VC_Client\VC_Client1」
  • C# 呼出元:「\InteropWithUnmanage\DNET\DNET_Client」

補足(現在ネイティブ DLL を作る意味): 本ページの技術は
今も必要な場面があるので、位置付けを整理しておく。

【今も C++ の DLL が要る場面】
   ・既存の C/C++ 資産(数値計算、画像処理、制御)を再利用する
   ・ハードウェア制御(ベンダー提供の SDK が C 言語)★
   ・性能が要求され、SIMD 等を直接使いたい
   ・OS の低レベル機能を使う

【逆に、不要になった場面】
   ・「速度のため C++ で書く」
      → 現在の .NET は【Span<T>、SIMD、Native AOT】で
        多くの場合十分速い ★
   ・「他言語から使わせるため」
      → REST / gRPC の方が疎結合で扱いやすい
        ([プロセス間通信](MS_InterProcessCommunication))

クロス プラットフォームを考えるなら:

・DLL(Windows)/ .so(Linux)/ .dylib(macOS)で
  【拡張子も検索規則も違う】
   → DllImport("mylib") と拡張子なしで書けば
     .NET が各 OS の規則で探す ★
   → NativeLibrary.SetDllImportResolver で明示的に解決もできる

・[アンマネージドコードのクロスプラットフォーム化](MS_UnmanagedCodeCrossPlatform)
  も参照

Tags: 移行, Windows, プログラミング, .NET開発

NetDevInfraWiki

マイクロソフト系技術情報 Wiki
Open 棟梁 Wiki

(未着手)

開発基盤部会 Wiki

移行管理: DONETODO

Clone this wiki locally