何もしないプラグインを作る

では実際にプラグインを作っていきます。いきなり全ての機能を実装し、説明するのは困難なのでまずはRegnessemに認識できるだけの何もしないプラグインを作ります。これに機能を追加していって、最終的にお留守番プラグインを完成させましょう。

下準備

C:\Programs以下にaamという作業ディレクトリを用意します。実際のソースをここにおいてコンパイルします。ディレクトリ構造は以下のようになるはずです。


    C:\Programs       
     +-aam            ; An Answering Machine(留守番電話)から命名
        +-nsmsgs      ; nsmsgsパッケージ
        |  +-consts.d ; consts(定数)モジュール
        |  +-types.d  ; types(型)モジュール
        +-aam.d       ; プラグインのメインソース
        +-aam.def     ; 定義ファイル
  

それぞれのファイルやディレクトリは後述するので今はC:\Programs\aamディレクトリがあればOKです。

nsmsgsパッケージ

C/C++言語でプラグインを作成するには、Regnessem公式サイト開発室からプラグインスケルトンをダウンロードすれば予めサービスやイベント、関数や構造体が定義されたヘッダファイルを入手する事が出来ますが、D言語のものは当然ない(笑)のでこちらで変換したものを用意しました。

この中には、nsmsgs\consts.dとnsmsgs\types.dが入っています。それぞれnsmsgsパッケージのconstsモジュール、typesモジュールと呼びます。

Dのモジュールは1つのソースファイルと1つのモジュールが1対1で対応します。つまり定数を定義してあるconsts.dはconstsモジュール、型を定義してあるtypes.dはtypesモジュールです。それらがnsmsgsディレクトリ以下に存在するので、nsmsgsパッケージに属していると言います。ディレクトリ構造がパッケージ構造を表しているのはJavaをやったことがある人にはなじみ深いかもしれません。

メインプログラム

D言語のソースファイルの拡張子にはわかりやすく.dと付けます。なのでメインプログラムのソースファイル名はaam.dとなります。以下にソースファイルの中身を書きます。


    module aam;
    
    private {
        import win32.ansi.windows;
        import nsmsgs.consts, nsmsgs.types;
        import std.string;
    }
    
    extern (C) {
        void gc_init();
        void gc_term();
        void _minit();
        void _moduleCtor();
        void _moduleUnitTests();
    }
    
    private const char[][] PluginInfo = [
        NSM_API_VERSION,                     // API Version
        "AddIn/aam",                         // Module Name
        "An Answering Machine on Regnessem", // Plugin Title
        "An Answering Machine on Regnessem", // Description
        "Moon",                              // Author
        "Copyright (c) 2004 Moon",           // Copyright
        "0.0.1",                             // Version
    ];
    
    extern (Windows)
    BOOL DllMain(HINSTANCE hInstance, ULONG ulReason, LPVOID pvReserved)
    {
        switch (ulReason) {
        case DLL_PROCESS_ATTACH:
            gc_init();          // init GC
            _minit();           // init module list
            _moduleCtor();      // run  module constructor
            _moduleUnitTests(); // run  unit test
            break;
        case DLL_PROCESS_DETACH:
            gc_term();
            break;
        case DLL_THREAD_ATTACH:
        case DLL_THREAD_DETACH:
            // multi-thread is not supported
            return FALSE;
        }
        return TRUE;
    }
    
    // DLL Export Functions
    
    extern (Windows)
    int GetPluginInfo(int nInfoNo, LPTSTR lpBuffer, int nSize)
    {
        if (nInfoNo < 0 || nInfoNo > PluginInfo.length) {
            // Unknown Information Number
            return 0;
        } else {
            lstrcpyn(lpBuffer, toStringz(PluginInfo[nInfoNo]), nSize);
            return lstrlen(lpBuffer);
        }
    }
    
    extern (Windows)
    int Initialize(PNsmPluginInitInfo lpInitInfo)
    {
        return 0;
    }
    
    extern (Windows)
    int Terminate()
    {
        return 0;
    }
  

module


    module aam;
  

これは、このソースのモジュール名を表します。先に書いたように、1つのソースは1つのモジュール名を持ちます。明示的に宣言しなくともファイルとディレクトリの構造から自動的にモジュール名が割り当てられますが、分かり易いように書いてあります。

private

privateは、その名の通りプライベートなスコープを持ちます。つまり、privateの付けられたものは外部から参照できないというわけです。

import


    private {
        import win32.ansi.windows;
        import nsmsgs.consts, nsmsgs.types;
        import std.string;
    }
  

importは他のモジュールを組み込む時に使います。import win32.ansi.windows;はwin32パッケージのansiパッケージのwindowsモジュールです。これは最初に用意したY.Tomino氏のWindows Portingです。

importに与えるモジュール名は、コンマで区切って複数並べることができます。import nsmsgs.consts, nsmsgs.types;はnsmsgsパッケージのconstsモジュールとtypesモジュールをインポートしています。

std.stringモジュールは、D言語の基本ライブラリ(phobos)で提供される文字列操作用のモジュールです。D言語の文字列とC言語の文字列は少し違うので、ここでは主にその相互変換のために使います。

extern

これは呼び出し規約を決めるものです。以下のコードはCの呼び出し規約を使って呼び出されます。各関数は、D言語が実装しているガベージコレクタを使うために宣言しています。


    extern (C) {
        void gc_init();
        void gc_term();
        void _minit();
        void _moduleCtor();
        void _moduleUnitTests();
    }
  

externは他にも、Cでいう_stdcallのためのWindowsがあります。詳しくは仕様書を見て下さい。少なくともDでRegnessemのプラグインを作る際に必要なのはextern (C)extern (Windows)です。

プラグイン情報


    private const char[][] PluginInfo = [
        NSM_API_VERSION,                     // API Version
        "AddIn/aam",                         // Module Name
        "An Answering Machine on Regnessem", // Plugin Title
        "An Answering Machine on Regnessem", // Description
        "Moon",                              // Author
        "Copyright (c) 2004 Moon",           // Copyright
        "0.0.1",                             // Version
    ];
  

privateなスコープを持つ、char型の2次元配列定数(const)を定義しています。これはRegnessemがプラグイン情報を得るときに使われます。ここがまずC言語との違いを実感させられる部分だと思います。Cでは2次元配列はchar foo[][]という風に変数名に後付しますが、Dでは型に配列を付けます(char[][] foo)。更に初期化する場合、Cでは{ ... }を使いましたがDでは[ ... ]を使います。参考のために以下にC/C++言語で同じものを示します。


    const char PluginInfo[][] = {
        NSM_API_VERSION,                     // API Version
        "AddIn/aam",                         // Module Name
        "An Answering Machine on Regnessem", // Plugin Title
        "An Answering Machine on Regnessem", // Description
        "Moon",                              // Author
        "Copyright (c) 2004 Moon",           // Copyright
        "0.0.1",                             // Version
    }
  

実のところ、DではCと同じように配列を前置(char[])でなく、後置(char a[])も可能です。つまり、先に挙げたコードでも後に挙げたコードでも同じようにコンパイルできます。これはCからの移行を簡単にするためです。

D言語自体の話が続きましたが、次にこの内容について説明しましょう。

0番目(PluginInfo[0])の値NSM_API_VERSIONはnsmsgs.constsモジュール内で"0.2.3"と定義されています。これは現在のRegnessemのAPIバージョンです。

1番目(PluginInfo[1])の値はモジュールの名前です。AddInプラグインのaamモジュールです。ここでいうモジュールはD言語のモジュールではなくRegnessemで使われるモジュールのことです。

2,3番目はプラグインの名前とプラグインの説明文です。任意に付けれるのでここでは"An Answering Machine on Regnessem"と付けています。

4,5番目はプラグインの作者名と著作権情報です。これらも任意に付けることが出来ます。

6番目はプラグインのバージョン文字列です。とりあえずここでは0.0.1としておきました。

DLLエントリポイント

DllMainはDLLのエントリポイントです。つまり通常のプログラムでいうmainWinMainと同じものです。プロセスにアタッチされたときにガベージコレクタを開始し、プロセスがデタッチしたときにガベージコレクタを終了しています。これはD仕様書のサンプルそのままです。

GetPluginInfo


    extern (Windows)
    int GetPluginInfo(int nInfoNo, LPTSTR lpBuffer, int nSize)
    {
        if (nInfoNo < 0 || nInfoNo > PluginInfo.length) {
            // Unknown Information Number
            return 0;
        } else {
            lstrcpyn(lpBuffer, toStringz(PluginInfo[nInfoNo]), nSize);
            return lstrlen(lpBuffer);
        }
    }
  

GetPluginInfo関数はRegnessem本体がプラグインの情報を取得するために呼ばれます。nInfoNoに取得する情報番号、lpBufferに文字列を格納するためのバッファ、nSizeにバッファのサイズが入って呼び出されます。

API仕様書によると、バージョン0.2.3ではバージョン情報の番号は0~6と決まっています。そこでそれ以外の値がきた場合は無効なので0を返します。値を格納した場合はlpBufferに対して書き込んだ文字列を戻り値として返します。

PluginInfo.lengthPluginInfo配列の長さを取得するプロパティです。0~6以外の情報番号の他に、PluginInfoで設定した値がない場合も無効なので0を返します。

lstrcpynはn文字をバッファにコピーするWindowsの関数で、ここで渡されたバッファに対して対応するプラグイン情報を書き込みます。そして戻り値としてlstrlen(文字列の長さを取得するWindows関数)で文字列の長さ(=書き込んだバイト数)を返しています。

toStringzはstd.stringモジュールのD言語の文字列をC言語の文字列に変換する関数です。引数にD言語の文字列を与えると、"\0"終端を持つ文字列(=C言語文字列)を返します。

ここでD言語の文字列とC言語の文字列の違いについて少し説明しておきます。C言語の文字列は null terminated という最後が\0で終わる文字配列です。それに対してD言語では終端が\0である必要はありません。なぜならDの配列は.lengthプロパティで文字列の長さがわかるためです。以下の図を見れば分かり易いと思います。


    char foo[] = "abc"; // C言語文字列 foo = { 'a', 'b', 'c', '\0' }
    char[] foo = "abc"; // D言語文字列 foo = { 'a', 'b', 'c' } (前置版)
    char foo[] = "abc"; // D言語文字列 foo = { 'a', 'b', 'c' } (後置版)
  

しかし、C言語の関数に文字列を渡す場合は null terminate してやる必要があります。そこで登場するのがtoStringz関数です。


    char[] foo = "abs";          // foo = { 'a', 'b', 'c' }
    char*  bar = toStringz(foo); // bar = { 'a', 'b', 'c', '\0' }
  

Initialize


    extern (Windows)
    int Initialize(PNsmPluginInitInfo lpInitInfo)
    {
        return 0;
    }
  

Initialize関数は、Regnessem本体が一度だけプラグインを初期化するために呼び出します。ここでサービスを作ったり、イベントをフックしたりしますが、今回は何もしないプラグインなので当然何もしません。正常終了したことを示す0を返しているだけです。

Terminate


    extern (Windows)
    int Terminate()
    {
        return 0;
    }
  

Terminate関数はプラグインが破棄される前にRegnessem本体から呼び出されます。ここでフックしたイベントを解放したりしますが、Initialize関数で何もしていないのでここでも何もしません。正常終了したことを示す0を返しています。

モジュール定義ファイル

さて、上記のソースをそのままコンパイルしてもDLLはできあがりません。コンパイラに対してこのソース群がどのようなものかを定義した定義ファイルを作る必要があります。


    LIBRARY "aam.dll"
    DESCRIPTION 'An Answering Machine on Regnessem'
    EXETYPE NT
    CODE PRELOAD DISCARDABLE
    DATA PRELOAD SINGLE
    EXPORTS
        GetPluginInfo
        Initialize
        Terminate
  

詳しい説明は省きますが、LIBRARYに作成するDLLの名前を、DESCRIPTIONに作成するプラグインの説明を、EXPORTSにDLLがexportする関数を記述します。基本的にRegnessemのプラグインの場合はGetPluginInfo,Initialize,Terminateの3つの関数をexportすればOKです。

コンパイル

これで全てのファイルが揃いました。早速コンパイルしてみましょう。


    C:\>cd \Programs\aam
    
    C:\Programs\aam>dmd -v aam.d aam.def nsmsgs\consts.d nsmsgs\types.d win32a.lib
    
    parse     aam
    parse     consts
    parse     types
    semantic  aam
    semantic  consts
    semantic  types
    semantic2 aam
    semantic2 consts
    semantic2 types
    semantic3 aam
    semantic3 consts
    semantic3 types
    code      aam
    generating code for function 'DllMain'
    generating code for function 'GetPluginInfo'
    generating code for function 'Initialize'
    generating code for function 'Terminate'
    code      consts
    code      types
    C:\dmd\bin\..\..\dm\bin\link.exe aam+consts+types,,,win32a.lib+user32+kernel32,aam.def/noi;
    
    C:\Programs\aam>
  

-vオプションはあってもなくても構いませんが、これを付けるとコンパイルメッセージがずらずらと表示されます。エラーがなければ上記のように終了するはずです。正常に終了するとファイルが以下のように出来ているはずです。


    C:\Programs
     +-aam
        +-nsmsgs
        |  +-consts.d
        |  +-types.d
        +-aam.d
        +-aam.def
        +-aam.obj
        +-consts.obj
        +-types.obj
        +-aam.map
        +-aam.DLL
  

作成されたプラグインDLLをRegnessemのPluginsフォルダにコピーしてRegnessemを起動します。メインメニューのヘルプからバージョン情報を開くときちんと以下のようなプラグイン情報が表示されているはずです。


    "An Answering Machine on Regnessem" 0.0.1
    Copyright (c) 2004 Moon
  

Download

参考リンク

Copyright © 2004 Moon moon@users.sourceforge.jp