Improve this page Github へのログインが必要です。 簡単な修正は、ここから fork、オンライン編集、pull request ができます。 大きな修正については、 通常の clone で行って下さい。 Page wiki 関連するWikiページを参照・編集 English このページの英語版(原文)

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;
}

注:

ビルド時には、次のような (モジュール定義ファイル) をリンクします:
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"); }

注: 例を可能な限り簡単にするために、 ここでは printfwritefln の代わりに使用しています。

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の呼び出し元が一切知る必要がないように工夫する必要があります。

この問題に対しては沢山のアプローチがあります:

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 関数をエクスポートしています。

mydll.dll DLL をビルドするには:
  1. dmd -c mydll -g
    mydll.dmydll.obj へコンパイル。 -g でデバッグ情報を生成します。
  2. dmd mydll.obj mydll.def -g -L/map
    mydll.objmydll.dll という名前のDLLへとリンクします。 mydll.defモジュール定義ファイル で、以下のような内容を記述しておきます:
    LIBRARY         MYDLL
    DESCRIPTION     'MyDll demonstration DLL'
    EXETYPE		NT
    CODE            PRELOAD DISCARDABLE
    DATA            PRELOAD MULTIPLE
    
    -g でデバッグ情報を生成し、 -L/map でマップファイル mydll.map を生成します。
  3. 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.exemydll.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:>