サービスを作る

プラグインにメニュー操作を実装するためにRegnessemのサービスの作り方を説明します。

予備知識

予備知識として、D言語での文字コードについて説明します。

DのソースファイルはUTF-8で記述する必要があります。なぜならchar型はUTF-8、wchar型はUTF-16として扱っているからです。Cのソースファイルは対象のプラットフォームに依存した文字コードを与える方法と、ワイド文字を使う方法があります。単純にアルファベットを扱う場合はUTF-8とASCIIコードは互換性があるので問題ないのですが、例えば日本語を扱おうとすると日本語WindowsではShift_JISが対象になります。

このことからソースファイルに例えば日本語を書き込んだ場合、CとDでは次のような違いが出てしまいます。


    char foo[] = "あ";  // C言語の場合 foo = { 0x82, 0xa0, '\0'} charは1バイト(Shift JIS)
    char foo[] = "あ";  // D言語の場合 foo = { 0x0e3820 }        UTF-8形式1文字
  

これを考えずに、Shift_JISのままソースに記述してコンパイルすると、当然日本語がエンコードの違いで文字化けを起こしてしまいます。DにはWYSIWYG文字列を扱うこともできますが、どうやら最新版のコンパイラではソースファイル自体がUnicodeで書かれていないとコンパイルエラーを起こすようです。ですからxyzzy等のUnicodeに対応したエディタでソースを編集して下さい。

ファイル構成

それでは今回のファイル構成です。wstring.dというファイルが増えています。


    C:\Programs
     +-aam
        +-nsmsgs
        |  +-consts.d
        |  +-types.d
        +-aam.d
        +-wstring.d
        +-aam.def
  

wstring.d

今回新たに作成するwstring.dは、wchar型をサポートするtoStringzのようなものを実装します。前回説明したようにPhobosにはstd.stringでC言語風文字列へ変換するtoStringzがありましたが、残念ながらこれはwchar型に対応していません。そこでPhobosのstd.stringを参考にtoStringzもどきを作成します。


    module wstring;
    
    wchar* toWStringz(wchar[] string)
    {
        wchar* p;
        wchar[] copy;
        
        if (string.length == 0) {
            return "";
        }
        
        p = &string[0] + string.length;
        
        if (*p == 0) {
            return string;
        }
        
        copy = new wchar[string.length + 1];
        copy[0..string.length] = string;
        copy[string.length] = '\0';
        return copy;
    }
  

この関数はwchar型の配列を引数にもち、wchar型のポインタを返す関数です。引数の文字列の長さが0であれば空文字を返します。次に引数の文字列の最後が0であるか、すなわち既に Null Terminate であるかを調べ、そうであればそのまま返します。そして最後に、元の文字列の長さよりも1文字多い配列を確保し、そこへ元の文字列をコピーして終端に\0を入れて返します。


    wchar[] foo = "あいうえお";
    wchar*  bar = toWStringz(foo); // bar = "あいうえお\0"
  

今までC++をあつかったことがある人は少し違和感を感じるかもしれません。何故ならnewでメモリを確保しているにも関わらずdeleteしていないからです。これはDがGCをサポートしているために、明示的にdeleteしなくとも、どこからも参照される事がなくなった時点で自動的にメモリを解放してくれるからです。これによりメモリリークの心配が格段に減ります。

メインメニューを扱うサービス

プラグインAPI仕様書を読むと、アドインモジュールはシステムが認識できる4つのサービスを提供することができます。

このサービスのうち、メインメニューを扱うのはAddIn/%name%/UIService/Main/%sです。これを今回のプラグインに適用するとなると、まず%name%はプラグインの名前であるaamになります。最後の%sは任意文字列です。%sには切り替えの意味でSwitchと付けることにしましょう。


    private const char[] NMS_ADDIN_AAM_UISERVICE_MAIN_SWITCH = "AddIn/aam/UIService/Main/Switch";
  

aam.d

それでは今回のメインソースです。前回から変更があった部分を色づけしてあります。


    module aam;
    
    private {
        import win32.ansi.windows;
        import nsmsgs.consts, nsmsgs.types;
        
        import wstring;
        
        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.2",                             // Version
            
        ];
        
        const wchar[][] MainMenu = [
            "留守番機能 ON",
            "留守番機能 OFF",
        ];
        static bool MenuStatus;
        
    }
    
    private const char[] NMS_ADDIN_AAM_UISERVICE_MAIN_SWITCH = "AddIn/aam/UIService/Main/Switch";
    
    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)
    {
        
        // Create Plugin Services
        lpInitInfo.CreateService(toStringz(NMS_ADDIN_AAM_UISERVICE_MAIN_SWITCH), &DoSwitch);
        
        return 0;
    }
    
    extern (Windows)
    int Terminate()
    {
        return 0;
    }
    
    
    // Services
    
    extern (Windows)
    int DoSwitch(WPARAM wParam, LPARAM lParam)
    {
        HNsmConnection    hConnection = cast(HNsmConnection)    wParam;
        PNsmUIServiceInfo lpUISvcInfo = cast(PNsmUIServiceInfo) lParam;
        
        switch (lpUISvcInfo.nInfoKey) {
        case NMUI_GETCAPTION:
            lstrcpynW(
                cast(wchar*) lpUISvcInfo.lpInfo.lpBuffer,
                toWStringz(MainMenu[(MenuStatus ? 1 : 0)]),
                lpUISvcInfo.lpInfo.nBufferSize);
            break;
        case NMUI_ONCLICK:
            MenuStatus = !MenuStatus;
            break;
        default:
            // Unknown Type
            return 0;
        }
        return 1;
    }
    
  

Initialize


    extern (Windows)
    int Initialize(PNsmPluginInitInfo lpInitInfo)
    {
        // Create Plugin Services
        lpInitInfo.CreateService(toStringz(NMS_ADDIN_AAM_UISERVICE_MAIN_SWITCH), &DoSwitch);
        return 0;
    }
  

プラグインを初期化するために呼ばれるInitialize関数が変更になっています。Initialize関数の引数PNsmPluginInitInfoTNsmInitInfo構造体のポインタです。ここにはシステム関数のポインタが格納されています。ここからシステム関数CreateServiceを呼び出してサービスを作成します。

CreateService関数の第一引数にはサービス名の文字列を、第二引数には実際に処理を行うサービス関数へのポインタを与えます。

今回、作成するサービス名はAddIn/aam/UIService/Main/Switchで、それを処理する関数がDoSwitch関数なのでその関数へのポインタを与えています。

DoSwitch


    extern (Windows)
    int DoSwitch(WPARAM wParam, LPARAM lParam)
    {
        HNsmConnection    hConnection = cast(HNsmConnection)    wParam;
        PNsmUIServiceInfo lpUISvcInfo = cast(PNsmUIServiceInfo) lParam;
        
        switch (lpUISvcInfo.nInfoKey) {
        case NMUI_GETCAPTION:
            lstrcpynW(
                cast(wchar*) lpUISvcInfo.lpInfo.lpBuffer,
                toWStringz(MainMenu[(MenuStatus ? 1 : 0)]),
                lpUISvcInfo.lpInfo.nBufferSize);
            break;
        case NMUI_ONCLICK:
            MenuStatus = !MenuStatus;
            break;
        default:
            // Unknown Type
            return 0;
        }
        return 1;
    }
  

AddIn/aam/UIService/Main/Switchサービスが呼ばれた時に処理する関数です。

AddIn/%name%/UIService/Main/%sサービスの引数は第一引数がHNsmConnection(コネクションハンドル)、第二引数がTNsmUIServiceInfo構造体へのポインタです。そこで、呼び出されたときにまず型キャストを行っています。基本的にWPARAM,LPARAMで呼び出されますのでそのままでは扱えないためです。

このサービスが呼ばれるときには、TNsmUIServiceInfo構造体のnInfoKeyメンバに呼び出し理由が入っています。APIバージョン0.2.3では次の呼び出し理由があります。

定数意味
0NMUI_GETCAPTIONメニューに表示する項目の取得
1NMUI_ONCLICKメニューが選択された

システムからキャプションを取得しようとして呼ばれた時の処理が以下です。


    case NMUI_GETCAPTION:
        lstrcpynW(
            cast(wchar*) lpUISvcInfo.lpInfo.lpBuffer,
            toWStringz(MainMenu[(MenuStatus ? 1 : 0)]),
            lpUISvcInfo.lpInfo.nBufferSize);
        break;
  

lstrcpynWは前回のlstrcpyn関数のワイド文字版です。TNsmUIServiceInfoのメンバlpInfoのメンバlpBufferには予め領域がnBufferSize分だけ確保されているのでそこにコピーします。

RegnessemのWideString型はUTF-16となっています。これは丁度Dのwchar型がUTF-16なのでそのままコピーしてやればいいのです。ソースファイルはUTF-8で記述しましたが、wchar型への代入は自動的にUTF-16にコンパイラが変更してくれます。ですので次のコード自体はUTF-8でも変数の中身はUTF-16であることが保証されます。


    const wchar[][] MainMenu = [
        "留守番機能 ON",  // UTF-8のソースだけどwchar[]に代入された時点でUTF-16
        "留守番機能 OFF", 
    ];
  

ここで気になるのはMenuStatus ? 1 : 0です。Cのプログラムを書くときに口を酸っぱくして変数は初期化しなさいと言われ続けた事とは思いますが、このソース中ではどこにもMenuStatus変数を初期化していません。これは、Dでは変数が宣言されたときにデフォルトで初期化される値が決まっているからです。これにより初期化忘れを防ぐことが出来ます。

次にシステムがメニューをクリックしたときに呼び出される処理が以下です。


    case NMUI_ONCLICK:
        MenuStatus = !MenuStatus;
        break;
  

MenuStatusの値を反転させているだけです。ですので、最初に呼ばれたときはfalseからtrueに、次はその逆に…と延々繰り返すだけです。この度にシステムはまたNMUI_GETCAPTIONでこのサービスを呼び出すのでメニューの文字列が選ばれるたびに文字列が変更されるわけです。

コンパイル

それでは今回のファイルをコンパイルしましょう。


    C:\>cd \Programs\aam
    
    C:\Programs\aam>dmd -v aam.d aam.def wstring.d nsmsgs\consts.d nsmsgs\types.d win32a.lib
    
    parse     aam
    parse     wstring
    parse     consts
    parse     types
    semantic  aam
    semantic  wstring
    semantic  consts
    semantic  types
    semantic2 aam
    semantic2 wstring
    semantic2 consts
    semantic2 types
    semantic3 aam
    semantic3 wstring
    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'
    generating code for function 'DoSwitch'
    code      wstring
    generating code for function 'toWStringz'
    code      consts
    code      types
    C:\dmd\bin\..\..\dm\bin\link.exe aam+wstring+consts+types,,,win32a.lib+user32+kernel32,aam.def/noi;
    
    C:\Programs\aam>
  

そして出来たファイルをRegnessemのPluginsフォルダにコピーして起動すればきちんとメニューに項目が追加され、ON/OFFの切り替えが出来るようになっていると思います。

Download

参考リンク

Copyright © 2004 Moon moon@users.sourceforge.jp