-
Notifications
You must be signed in to change notification settings - Fork 0
MS_DLLCreationSteps
- 戻る(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.cpp(DllMain)についての重要な注意:【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; // 例外を境界の外へ出さない } }
- 参考:Advanced Windows
- 第 4 部 DLL
- 第 19 章 DLL の基礎
- 第 20 章 DLL の高度なテクニック
- 第 4 部 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("") でカレント ディレクトリを外す
- VC++ からの DLL 呼び出しでは、*.lib ファイルを使用した暗黙のリンクを使用している。
- *.lib ファイルを参照する場合は、以下の手順に従う。
- リンカ入力としての .lib ファイル
https://learn.microsoft.com/ja-jp/cpp/build/reference/dot-lib-files-as-linker-input- プロジェクトの [プロパティ ページ] ダイアログ ボックスを開きます。
- [リンカ] フォルダをクリックします。
- [入力] プロパティ ページをクリックします。
- [追加の依存ファイル] プロパティを変更します。
- リンカ入力としての .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;
}.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 で問題になる ・生成コードを読めるので、挙動が追える
-
Codian(DLL・フック)
http://www.kab-studio.biz/Programing/Codian/#DLL_Hook_SClass- DLLを使おう!!
http://www.kab-studio.biz/Programing/Codian/DLL_Hook_SClass/01.html - DLLの実行中リンク
http://www.kab-studio.biz/Programing/Codian/DLL_Hook_SClass/02.html - DLLを作ろう!(関数編)
http://www.kab-studio.biz/Programing/Codian/DLL_Hook_SClass/03.html - 「自作」DLLを使おう!!
http://www.kab-studio.biz/Programing/Codian/DLL_Hook_SClass/04.html - DLLを作ろう!(クラス編)
http://www.kab-studio.biz/Programing/Codian/DLL_Hook_SClass/05.html
- DLLを使おう!!
-
Win32API(C言語)編 トップページ
- Win32API(C言語)編 第57章 ダイナミックリンクライラリ(DLL)
(リンク切れ:Yahoo!ジオシティーズはサービス終了) - Win32API(C言語)編 第58章 LoadLibrary()によるDLLロード
(リンク切れ:同上) - Win32API(C言語)編 第59章 .DEFファイルによるDLLエクスポート
(リンク切れ:同上)
- Win32API(C言語)編 第57章 ダイナミックリンクライラリ(DLL)
-
ダイナミック リンク ライブラリ(DLL)の基礎知識
http://exlight.net/devel/windows/dll/windll.html -
VC++DLL作成補足(Hishidama's VC++Memo DLL)
https://www.ne.jp/asahi/hishidama/home/tech/vcpp/dllusage.html -
C++で作成したDLLを他の言語で利用できるようにする » 二流プログラマの三流な日常
http://blogs.konuma.org/blog/2007/04/c_b43e/
-
MYLIBAPI
- 上記コード中で定義されるマクロです。
- 当該(DLL の)プロジェクトではエクスポートを実行
- (DLL の)利用側プロジェクトではインポートを実行
-
extern "C"
関数がC言語形式 になっていることを示す。 -
__stdcall = WINAPI
- Web-DB プログラミング徹底解説
- Hello, world の解説 ~ WINAPI とは何か? __stdcall の説明
http://keicode.com/winprimer/wp07.php - 関数を呼び出すということ
http://keicode.com/winprimer/wp07b.php
- Hello, world の解説 ~ WINAPI とは何か? __stdcall の説明
- Web-DB プログラミング徹底解説
-
__declspec(dllexport)
- DLL からのエクスポート
https://learn.microsoft.com/ja-jp/cpp/build/exporting-from-a-dll- __declspec(dllexport) を使った DLL からのエクスポート
https://learn.microsoft.com/ja-jp/cpp/build/exporting-from-a-dll-using-declspec-dllexport
- __declspec(dllexport) を使った DLL からのエクスポート
- DLL からのエクスポート
-
__declspec(dllimport)
- アプリケーションへのインポート
https://learn.microsoft.com/ja-jp/cpp/build/importing-into-an-application- __declspec(dllimport) を使った関数呼び出しのインポート
https://learn.microsoft.com/ja-jp/cpp/build/importing-function-calls-using-declspec-dllimport
- __declspec(dllimport) を使った関数呼び出しのインポート
- アプリケーションへのインポート
-
モジュール定義ファイル、LIBRARY、EXPORTS
- DEF ファイルを使った DLL からのエクスポート
https://learn.microsoft.com/ja-jp/cpp/build/exporting-from-a-dll-using-def-files
- DEF ファイルを使った 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開発
このWikiは「Open棟梁Project」,「OSSコンソーシアム 開発基盤部会」によって運営されています。