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;
import core.sys.windows.dll;
__gshared HINSTANCE g_hInst;
extern (Windows)
BOOL DllMain(HINSTANCE hInstance, ULONG ulReason, LPVOID pvReserved)
{
switch (ulReason)
{
case DLL_PROCESS_ATTACH:
g_hInst = hInstance;
dll_process_attach( hInstance, true );
break;
case DLL_PROCESS_DETACH:
dll_process_detach( hInstance, true );
break;
case DLL_THREAD_ATTACH:
dll_thread_attach( true, true );
break;
case DLL_THREAD_DETACH:
dll_thread_detach( true, true );
break;
}
return true;
}
注:
- DllMain では、適切なヘルパ関数への単純な転送のみを行います。 これによってランタイムが正しく設定され、 GCやスレッドローカル記憶域の処理に関連するスレッドオブジェクトが生成されます。
- DLL は他のDLLとランタイムやメモリを共有しません。
- ヘルパ関数への第一引数のbool値は、 全スレッドがGCにコントロールされるべきかを示すフラグです。 もし、GC中も停止したくないスレッドがあれば、手動制御が必要です。 その場合、自動制御を切るためにfalseを渡して下さい。
- DllMain() の存在はコンパイラが認識し、 __acrtused_dll と phobos.lib ランタイムライブラリへの参照を自動的に追加します。
LIBRARY MYDLL
DESCRIPTION 'My DLL written in D'
EXETYPE NT
CODE PRELOAD DISCARDABLE
DATA WRITE
EXPORTS
DllGetClassObject @2
DllCanUnloadNow @3
DllRegisterServer @4
DllUnregisterServer @5
EXPORTS の中に並べた関数名は一例です。 実際にMYDLLからexportしたい関数の名前に置き換えてください。 あるいは、 implib. を使います。以下に、文字列を出力する print() 関数を備える、 簡単なDLLの例を示します:
mydll.d:
module mydll;
import std.c.stdio;
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 -L/IMPLIB mydll.d dll.d mydll.def
C:>
これで、mydll.dll と mydll.lib が作られます。 次に、このDLLを使うプログラム、test.d です:
test.d:
import mydll;
int main()
{
mydll.dllprint();
return 0;
}
関数定義を消した、宣言だけのインターフェイスファイルを作ります:
mydll.di:
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に伝えるプロトコルを 用意しておく。
- GC.addRange を呼び出すことで、GCに、 外部からの参照の存在を伝える。
- DLLの外とやりとりするメモリについては、例えば VirtualAlloc() のような OSの機能を直接使って割り当てる。
- U呼び出し元へ返されるメモリの割り当てには 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 core.runtime;
import std.c.stdio;
import std.c.stdlib;
import std.string;
import std.c.windows.windows;
HINSTANCE g_hInst;
extern (C)
{
void gc_setProxy(void* p);
void gc_clrProxy();
}
extern (Windows)
BOOL DllMain(HINSTANCE hInstance, ULONG ulReason, LPVOID pvReserved)
{
switch (ulReason)
{
case DLL_PROCESS_ATTACH:
printf("DLL_PROCESS_ATTACH\n");
Runtime.initialize();
break;
case DLL_PROCESS_DETACH:
printf("DLL_PROCESS_DETACH\n");
Runtime.terminate();
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");
gc_setProxy(gc);
}
export void MyDLL_Terminate()
{
printf("MyDLL_Terminate()\n");
gc_clrProxy();
}
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がCプログラムからでもDプログラムからでも使えるようにするためで、
同じ初期化プロセスがどちらの場合でも正しく動作するように作られています。
- MyDLL_Initialize
-
DLL が Runtime.loadLibrary() で動的にリンクされた場合、
ランタイムによって、Dプログラムに必要な初期化ステップがライブラリロード後に
実行されることが保証されます。
ライブラリが静的にリンクされた場合は、
プログラムからこの処理は呼び出されないため、
DLLを正しく動作させるためにいくつかの処理を自分で行う必要があります。
そして、静的リンクされているので、
DLLに固有の初期化処理関数が必要になります。
この関数は、呼び出し元のGCのハンドルを引数として取ります。
このハンドルを得る方法は後で説明します。
このハンドルをランタイムに渡してDLL組み込みのGCを上書きするには、
gc_setProxy() を呼び出します。
この関数は export されて、
DLLの外側から呼び出せるようになっています。
- MyDLL_Terminate
- 対応して、この関数はアンロードの前に呼び出され、
DLLの終了処理を担当します。
具体的な処理はひとつだけで、DLLが呼び出し側のGCを使うことはもうないと
gc_clrProxy() で通知します。
このステップは重要です。この後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 mydll.def -g -L/map
mydll.obj を mydll.dll という名前のDLLへとリンクします。 mydll.def は モジュール定義ファイル で、以下のような内容を記述しておきます:LIBRARY MYDLL DESCRIPTION 'MyDll demonstration DLL' EXETYPE NT CODE PRELOAD DISCARDABLE DATA PRELOAD MULTIPLE
-g でデバッグ情報を生成し、 -L/map でマップファイル mydll.map を生成します。 - implib /noi /system mydll.lib mydll.dll
静的に mydll.dll をロードするアプリケーションとのリンクに使う インポートライブラリ mydll.lib を作ります。
以下に、mydll.dll を使うサンプルアプリケーション test.d の例を示します。静的にDLLとリンクするバージョンと、 動的にロードするバージョンの2つが含まれています。
import core.runtime;
import std.stdio;
import std.gc;
import mydll;
//version=DYNAMIC_LOAD;
version (DYNAMIC_LOAD)
{
import std.c.windows.windows;
alias MyClass function() getMyClass_fp;
int main()
{ HMODULE h;
FARPROC fp;
getMyClass_fp getMyClass;
MyClass c;
printf("Start Dynamic Link...\n");
h = cast(HMODULE) Runtime.loadLibrary("mydll.dll");
if (h is null)
{
printf("error loading mydll.dll\n");
return 1;
}
fp = GetProcAddress(h, "D5mydll10getMyClassFZC5mydll7MyClass");
if (fp is null)
{ printf("error loading symbol getMyClass()\n");
return 1;
}
getMyClass = cast(getMyClass_fp) fp;
c = (*getMyClass)();
foo(c);
if (!Runtime.unloadLibrary(h))
{ printf("error freeing mydll.dll\n");
return 1;
}
printf("End...\n");
return 0;
}
}
else
{ // DLLは静的リンク
extern (C)
{
void* gc_getProxy();
}
int main()
{
printf("Start Static Link...\n");
MyDLL_Initialize(gc_getProxy());
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は、 Runtime.loadLibrary(), の呼び出しでロードし、 エクスポートされた関数それぞれは、 GetProcAddress() の呼び出しで取得します。 GetProcAddress() に渡すための修飾名を簡単に得るには、 生成された mydll.map ファイルの Export 以下の部分からコピー&貼り付けという方法があります。 一度この作業が終わると、DLL内クラスのメンバ関数も、 test.exe 内のものと同様に使うことができます。 終了時には、DLLを Runtime.unloadLibrary() で解放します。
実行結果は次のようになります:
C:>test
Start Dynamic Link...
DLL_PROCESS_ATTACH
static this for mydll
Hello world!
static ~this for mydll
DLL_PROCESS_DETACH
End...
C:>