では実際にプラグインを作っていきます。いきなり全ての機能を実装し、説明するのは困難なのでまずは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です。
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 aam;
これは、このソースのモジュール名を表します。先に書いたように、1つのソースは1つのモジュール名を持ちます。明示的に宣言しなくともファイルとディレクトリの構造から自動的にモジュール名が割り当てられますが、分かり易いように書いてあります。
private
は、その名の通りプライベートなスコープを持ちます。つまり、private
の付けられたものは外部から参照できないというわけです。
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言語の文字列は少し違うので、ここでは主にその相互変換のために使います。
これは呼び出し規約を決めるものです。以下のコードは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としておきました。
DllMain
はDLLのエントリポイントです。つまり通常のプログラムでいうmain
やWinMain
と同じものです。プロセスにアタッチされたときにガベージコレクタを開始し、プロセスがデタッチしたときにガベージコレクタを終了しています。これはD仕様書のサンプルそのままです。
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.length
はPluginInfo
配列の長さを取得するプロパティです。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' }
extern (Windows)
int Initialize(PNsmPluginInitInfo lpInitInfo)
{
return 0;
}
Initialize
関数は、Regnessem本体が一度だけプラグインを初期化するために呼び出します。ここでサービスを作ったり、イベントをフックしたりしますが、今回は何もしないプラグインなので当然何もしません。正常終了したことを示す0を返しているだけです。
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