D で作る Win32 DLL
DLL (Dynamic Link Libraries) は、 Windows のシステムプログラミングの基礎技術の一つです。 D言語では、様々な種類のDLLの作成が可能です。
DLLとは何でどのように動くのか、といった背景知識に関しては Jeffrey Richter の本 Advanced Windows の第11章が必読です。(訳注:邦訳)
この文書では、Dで様々な種類のDLLを作る方法を紹介します。
C のインターフェイスを持つ DLL
C言語インターフェイスを提供するDLLは、 DLL内のC関数を呼ぶ機能に対応した他の言語と連携できます。
DLL は D でも C とだいたい同じ方法で作れます。 次のような DllMain() を書きます:
import std.c.windows.windows; HINSTANCE g_hInst; extern (C) { void gc_init(); void gc_term(); void _minit(); void _moduleCtor(); void _moduleUnitTests(); } extern (Windows) BOOL DllMain(HINSTANCE hInstance, ULONG ulReason, LPVOID pvReserved) { switch (ulReason) { case DLL_PROCESS_ATTACH: gc_init(); // GC初期化 _minit(); // モジュールリスト初期化 _moduleCtor(); // モジュールコンストラクタ実行 _moduleUnitTests(); // 単体テスト実行 break; case DLL_PROCESS_DETACH: gc_term(); // GC終了 break; case DLL_THREAD_ATTACH: case DLL_THREAD_DETACH: // マルチスレッドは未対応です return false; } g_hInst=hInstance; return true; }
注:
- _moduleUnitTests() の呼び出しは無くても構いません。
- DllMain() の存在はコンパイラが認識し、 __acrtused_dll と phobos.lib ランタイムライブラリへの参照を自動的に追加します。
LIBRARY MYDLL DESCRIPTION 'My DLL written in D' EXETYPE NT CODE PRELOAD DISCARDABLE DATA PRELOAD SINGLE EXPORTS DllGetClassObject @2 DllCanUnloadNow @3 DllRegisterServer @4 DllUnregisterServer @5
EXPORTS の中に並べた関数名は一例です。 実際にMYDLLからexportしたい関数の名前に置き換えてください。 あるいは、 implib を使います。以下に、文字列を出力する print() 関数を備える、 簡単なDLLの例を示します:
mydll2.d:
module mydll; export void dllprint() { printf("hello dll world\n"); }
注: 例を可能な限り簡単にするために、 ここでは printf を writefln の代わりに使用しています。
mydll.def:
LIBRARY "mydll.dll" EXETYPE NT SUBSYSTEM WINDOWS CODE SHARED EXECUTE DATA WRITE
DllMain() を含む上記のコードは、 dll.d というファイルに書いたとします。 以下のようなコマンドでコンパイルとリンクを行います:
C:>dmd -ofmydll.dll mydll2.d dll.d mydll.def C:>implib/system mydll.lib mydll.dll C:>
これで、mydll.dll と mydll.lib が作られます。 次に、このDLLを使うプログラム、test.d です:
test.d:
import mydll; int main() { mydll.dllprint(); return 0; }
mydll2.dから関数定義だけを消したファイルを作ります:
mydll.d:
export void dllprint();そして以下でコンパイル・リンクし:
C:>dmd test.d mydll.lib C:>実行します:
C:>test hello dll world C:>
メモリ割り当て
DのDLLはメモリ管理にガベージコレクタを使います。問題は、 割り当てられたメモリを指すポインタがDLLの外で使われるとどうなるか、という点です。 DLLがCインターフェイスを提供するならば、他言語で書かれたコードから 呼び出されることを想定しなければなりません。 それら他の言語達は、Dのメモリ管理について一切タッチしていません。 つまり、Cインターフェイスを提供する以上、DLL内部のメモリ管理について DLLの呼び出し元が一切知る必要がないように工夫する必要があります。
この問題に対しては沢山のアプローチがあります:
- Dのgcが割り当てたメモリは、DLLの呼び出し元へ返さない。代わりに、 呼び出し元の割り当てたバッファを受け取って、そこをDLLで埋める インターフェイスにする。
- そのポインタをDのDLL内に保持して、GCによって回収されないようにする。 そして、呼び出し元がデータが不要になったことをDLLに伝えるプロトコルを 用意しておく。
- DLLの外とやりとりするメモリについては、例えば VirtualAlloc() のような OSの機能を直接使って割り当てる。
- 呼び出し元へ返されるメモリの割り当てには std.c.stdlib.malloc() (またはその他のGC外のアロケータ)を使う。 呼び出し元がデータを解放するための関数も export しておく。
COM プログラミング
Windows API の多くは、COM (Common Object Model) オブジェクト(OLE や ActiveX オブジェクトとも呼ばれる) に基づいています。COM オブジェクトとは、第一フィールドが vtbl[] へのポインタで、その最初の三つのエントリが QueryInterface(), AddRef(), Release() であるオブジェクトのことです。COM の理解には、Kraig Brockshmidt の Inside OLE (訳注:邦訳) が必読書です。
COMオブジェクトとDのinterfaceの間には類似性があります。COMオブジェクトは 皆Dのinterfaceとして表現できますし、interface Xを実装したDのオブジェクトは、 COMオブジェクトX として export できます。 これはつまり、Dは他の言語で実装された COMオブジェクトとの相互運用ができるということです。
絶対必要なわけではありませんが、Phobosライブラリは、DのCOMオブジェクト の基底クラスとして便利な、ComObjectクラスを提供しています。 ComObjectは QueryInterface(), AddRef(), Release() の標準的な実装を備えています。
Windows COM オブジェクトは、Dのデフォルトとは違う、 Windows呼び出し規約に従います。 このため、属性 extern (Windows) が必要です。 結論として、COMオブジェクトを書くには次のようになります:
import std.c.windows.com; class MyCOMobject : ComObject { extern (Windows): ... }サンプルディレクトリの中に、COMのクライアントとサーバDLLの例があります。
DLL内のDのコードを呼ぶDのコード
DのコードをDLL内に含めて、 静的リンクの場合と全く同様に扱えるようにする機能はもちろん必要です。 それによって、コードを別のDLLとして独立に開発し、 しかもアプリケーション間での共有が可能になります。潜在的に問題となるのは、ガベージコレクション(GC)の取り扱いです。 EXE と DLL のそれぞれがGCのインスタンスを保持しています。 これらのGC同士はお互いに影響せずに共存することも可能ですが、 複数のGCが走っているというのは、無駄で非効率的です。 そこで、GC一つを決めて、他のDLLのGCはそのGCへとリダイレクトするという 案を考えました。一つに決めるGCは、 ここでは EXE ファイルのものを使うこととしました。GC のために特別の DLL を一つ用意しておくという方法もあります。
以下は、DLLを静的にロードする方法と、 動的にロード/アンロードする方法の両方の例になっています。
DLLのソースコード mydll.d から見ていきましょう:
/* * MyDll D言語DLLの書き方デモ */ import std.c.stdio; import std.c.stdlib; import std.string; import std.c.windows.windows; import std.gc; HINSTANCE g_hInst; extern (C) { void _minit(); void _moduleCtor(); void _moduleDtor(); void _moduleUnitTests(); } extern (Windows) BOOL DllMain(HINSTANCE hInstance, ULONG ulReason, LPVOID pvReserved) { switch (ulReason) { case DLL_PROCESS_ATTACH: printf("DLL_PROCESS_ATTACH\n"); break; case DLL_PROCESS_DETACH: printf("DLL_PROCESS_DETACH\n"); std.c.stdio._fcloseallp = null; // 標準入出力を閉じないように break; case DLL_THREAD_ATTACH: printf("DLL_THREAD_ATTACH\n"); return false; case DLL_THREAD_DETACH: printf("DLL_THREAD_DETACH\n"); return false; } g_hInst = hInstance; return true; } export void MyDLL_Initialize(void* gc) { printf("MyDLL_Initialize()\n"); std.gc.setGCHandle(gc); _minit(); _moduleCtor(); // _moduleUnitTests(); } export void MyDLL_Terminate() { printf("MyDLL_Terminate()\n"); _moduleDtor(); // モジュールデストラクタを実行 std.gc.endGCHandle(); } static this() { printf("static this for mydll\n"); } static ~this() { printf("static ~this for mydll\n"); } /* --------------------------------------------------------- */ class MyClass { char[] concat(char[] a, char[] b) { return a ~ " " ~ b; } void free(char[] s) { delete s; } } export MyClass getMyClass() { return new MyClass(); }
- DllMain
- 全てのD言語DLLのエントリーポイントはこの関数です。
Cのスタートアップコードから呼び出されます。
(DMC++でいうと、ソースは \dm\src\win32\dllstart.c です)。
printf の表示結果で、
どのように呼び出されるかがわかります。
古い DllMain のサンプルコードにあった初期化と終了処理のコードが、
存在しなくなっていることにご注意下さい。
これは、初期化処理は、誰によってDLLがロードされようとしているのか、
あるいはどのようにロードされているのか (静的にか、動的にか) に依存するためです。
ここで行う処理はほとんどありません。
唯一奇妙に見える部分は、std.c.stdio._fcloseallp を null
に設定しているところです。これを null にしない場合、CランタイムはDLL終了時に
標準I/Oバッファ (stdout や
stderr など) をフラッシュしてから閉じ、
後の出力をやめてしまいます。nullにセットすることで、
標準I/Oを閉じる処理をDLLの呼び出し側にゆだねることができます。
- MyDLL_Initialize
- というわけで代わりに、いつ呼び出すかを制御できる、
独自のDLL初期化ルーチンを用意します。
このルーチンを呼び出すのは、呼び出し元がPhobosライブラリ、モジュールコンストラクタ、
と自分自身の初期化を終えた後でなければなりません。
(これは通常、main() に入った後ということになります)。
この関数は、呼び出し元のGCのハンドルを引数として取ります。
このハンドルを得る方法は後で説明します。
gc_init() を呼んでDLLのGCを初期化する代わりに、
std.gc.setGCHandle() を呼んで、
どのGCを使うべきかを渡します。
このステップによって、呼び出し元のGCに、
DLL内のうちスキャンすべきメモリ領域を伝えることになります。
その後は、_minit() 呼び出しによるモジュールテーブルの初期化が続き、
_moduleCtor() によってモジュールコンストラクタを実行します。
_moduleUnitTests() の呼び出しは省略も可能ですが、DLLの単体テストを実行します。
この関数は export されて、
DLLの外側から呼び出せるようになっています。
- MyDLL_Terminate
- 対応して、この関数はアンロードの前に呼び出され、
DLLの終了処理を担当します。
具体的には、二つの処理を行います。まず _moduleDtor() によって、
DLLのモジュールデストラクタを呼び出し、次に std.gc.endGCHandle() によって、
呼び出し側のGCはもう使う必要がないことを
ランタイムライブラリに通知します。
最後のステップは重要です。この後DLLがメモリからマップ解除された場合、
仮にGCがDLLのメモリ領域をスキャンしようとすると、
セグメント違反が発生してしまいます。
- static this, static ~this
- モジュールの
静的コンストラクタと静的デストラクタの例です。
実行と実行タイミングの確認のために、
文字列を表示します。
- MyClass
- DLLからエクスポートされ、
呼び出し側から使えるようにするクラスの例です。concat
メンバ関数はGCによるメモリ割り当てを行い、free
はGCメモリの解放を行います。
- getMyClass
- MyClass のインスタンスを割り当て参照を返す factory
関数をエクスポートしています。
- dmd -c mydll -g
mydll.d を mydll.obj へコンパイル。 -g でデバッグ情報を生成します。 - dmd mydll.obj \dmd\lib\gcstub.obj mydll.def -g -L/map
mydll.obj を mydll.dll という名前のDLLへとリンクします。 gcstub.obj は必須ではありませんが、これをリンクすることで、 不要なGCコードを省くことができます。 およそ12Kbの削減になります。 mydll.def は モジュール定義ファイル で、以下のような内容を記述しておきます:LIBRARY MYDLL DESCRIPTION 'MyDll demonstration DLL' EXETYPE NT CODE PRELOAD DISCARDABLE DATA PRELOAD SINGLE
-g でデバッグ情報を生成し、 -L/map でマップファイル mydll.map を生成します。 - implib /noi /system mydll.lib mydll.dll
静的に mydll.dll をロードするアプリケーションとのリンクに使う インポートライブラリ mydll.lib を作ります。
以下に、mydll.dll を使うサンプルアプリケーション test.d の例を示します。静的にDLLとリンクするバージョンと、 動的にロードするバージョンの2つが含まれています。
import std.stdio; import std.gc; import mydll; //version=DYNAMIC_LOAD; version (DYNAMIC_LOAD) { import std.c.windows.windows; alias void function(void*) MyDLL_Initialize_fp; alias void function() MyDLL_Terminate_fp; alias MyClass function() getMyClass_fp; int main() { HMODULE h; FARPROC fp; MyDLL_Initialize_fp mydll_initialize; MyDLL_Terminate_fp mydll_terminate; getMyClass_fp getMyClass; MyClass c; printf("Start Dynamic Link...\n"); h = LoadLibraryA("mydll.dll"); if (h == null) { printf("error loading mydll.dll\n"); return 1; } fp = GetProcAddress(h, "D5mydll16MyDLL_InitializeFPvZv"); if (fp == null) { printf("error loading symbol MyDLL_Initialize()\n"); return 1; } mydll_initialize = cast(MyDLL_Initialize_fp) fp; (*mydll_initialize)(std.gc.getGCHandle()); fp = GetProcAddress(h, "D5mydll10getMyClassFZC5mydll7MyClass"); if (fp == null) { printf("error loading symbol getMyClass()\n"); return 1; } getMyClass = cast(getMyClass_fp) fp; c = (*getMyClass)(); foo(c); fp = GetProcAddress(h, "D5mydll15MyDLL_TerminateFZv"); if (fp == null) { printf("error loading symbol MyDLL_Terminate()\n"); return 1; } mydll_terminate = cast(MyDLL_Terminate_fp) fp; (*mydll_terminate)(); if (FreeLibrary(h) == FALSE) { printf("error freeing mydll.dll\n"); return 1; } printf("End...\n"); return 0; } } else { // DLLは静的リンク int main() { printf("Start Static Link...\n"); MyDLL_Initialize(std.gc.getGCHandle()); foo(getMyClass()); MyDLL_Terminate(); printf("End...\n"); return 0; } } void foo(MyClass c) { char[] s; s = c.concat("Hello", "world!"); writefln(s); c.free(s); delete c; }
まず簡単な方の、静的リンクするバージョンから見ていきましょう。 次のようなコマンドでコンパイルとリンクを行います:
C:>dmd test mydll.lib -g
mydll.dll のインポートライブラリ mydll.lib とリンクしています。 コードは簡単です。test.exe のGCのハンドルを渡して MyDLL_Initialize() を呼び出し、 mydll.lib を初期化しています。 その後はDLL内の関数を、test.exeの中にあるのと全く同様に使うことが可能です。 foo() では、GC によるメモリの割り当てと解放が test.exe と mydll.dll の双方で行われています。 DLLを使い終わった後は、 MyDLL_Terminate() で終了します。
実行結果は次のようになります:
C:>test DLL_PROCESS_ATTACH Start Static Link... MyDLL_Initialize() static this for mydll Hello world! MyDLL_Terminate() static ~this for mydll End... C:>
動的リンクの方は準備が少し複雑です。 次のようなコマンドでコンパイルとリンクを行います:
C:>dmd test -version=DYNAMIC_LOAD -g
インポートライブラリ mydll.lib は不要です。 DLLは、 LoadLibraryA() の呼び出しでロードし、 エクスポートされた関数それぞれは、 GetProcAddress() の呼び出しで取得します。 GetProcAddress() に渡すための修飾名を簡単に得るには、 生成された mydll.map ファイルの Export 以下の部分からコピー&貼り付けという方法があります。 一度この作業が終わると、DLL内クラスのメンバ関数も、 test.exe 内のものと同様に使うことができます。 終了時には、DLLを FreeLibrary() で解放します。
実行結果は次のようになります:
C:>test Start Dynamic Link... DLL_PROCESS_ATTACH MyDLL_Initialize() static this for mydll Hello world! MyDLL_Terminate() static ~this for mydll DLL_PROCESS_DETACH End... C:>