◆VBA (VB6)でのWindows Service制御 - Win32 API編 その2

[ 上へ ]

・スタートアップの種類の取得の前準備 1

スタートアップの種類の取得は VB2005の事例 の時のようにレジストリを検索してもいいのですが、 APIにはそれを取得する関数が用意されています。 QueryServiceConfigがそれです。
第一引数はサービスのハンドル、
第二引数は結果を受け取るバッファ、
第三引数は第二引数のバッファが必要とするバイト数、
第四引数は第三引数で指定したバイト数が不足だった場合などに必要なサイズを受け取る変数です。

この関数の第二引数に QUERY_SERVICE_CONFIG構造体 を指定して、そのメンバであるdwStartTypeに該当サービスのスタートアップの種類が格納される仕様です。

とりあえず、 Win32Api.txt からこの関数と構造体をコピーしましょう。

Private Declare Function QueryServiceConfig Lib "advapi32.dll" _
                                            Alias "QueryServiceConfigA" _
                (ByVal hService As Long, _
                 lpServiceConfig As QUERY_SERVICE_CONFIG, _
                 ByVal cbBufSize As Long, _
                 pcbBytesNeeded As Long) As Long

Private Type QUERY_SERVICE_CONFIG
    dwServiceType      As Long
    dwStartType        As Long
    dwErrorControl     As Long
    lpBinaryPathName   As String
    lpLoadOrderGroup   As String
    dwTagId            As Long
    lpDependencies     As String
    lpServiceStartName As String
    lpDisplayName      As String
End Type

ここで注目してほしいのは QueryServiceConfig の第四引数です。ここは第二引数に指定した QUERY_SERVICE_CONFIG構造体 に入りきらないことによって関数が失敗したときに、いったい どれだけのバイト数が必要かを受け取るためにあります。 QUERY_SERVICE_CONFIG構造体 のサイズは第三引数で指定しますから、ここは

Dim vtSvcConf As QUERY_SERVICE_CONFIG
QueryServiceConfig(xxxx, xxxxx, LenB(vtSvcConf), xxx)

とすれば入りきらないことはないし、第四引数は必要ないように思われます。 もし、仮に第二引数に前回のSERVICE_STATUS構造体を指定するのでしたら、 LenB(SERVICE_STATUS構造体のインスタンス)で問題ないです。しかし、ここに指定するのは QUERY_SERVICE_CONFIG 型です。では、この2つの構造体はどう違うのでしょう?

2つの構造体をよくみてみてください。SERVICE_STATUS構造体のメンバはすべてLong型です。 Long型は4バイト(32ビット)固定です。したがって、そのサイズはメンバ数7 X 4 バイト =28バイト で一定です。
一方、 QUERY_SERVICE_CONFIG はString型が入っています。ということは、String型の各メンバは どれだけの文字列が格納されるかわからない可変長ですから、サイズは一定ではないです。 ということで QUERY_SERVICE_CONFIG 型に 結果を受け入れるためのサイズ は不定ということになります。
このサイズを受け取るのが QueryServiceConfig の第四引数ということになり、受け取った サイズを再度 QueryServiceConfig の第三引数に指定して、必要な情報を第二引数に受け取る ことになります。

ここで誤解しないでほしいのですが、先ほど QUERY_SERVICE_CONFIG 型の結果を受け入れるための サイズは不定と書きましたが、 QUERY_SERVICE_CONFIG型のサイズ は36バイトで一定です。何を言っている??と思われるかもしれませんが、 これを理解するのはC言語の知識が必要になってきます。

・ポインタ

最初に C言語でのAPIの宣言を確認しなければならないときがあると書きましたが今がそのときです。 VBA(VB6)なのにC言語を理解しなければならないのは苦痛ですが、APIを使う以上避けられないです。

C言語での QUERY_SERVICE_CONFIG の定義は

typedef struct _QUERY_SERVICE_CONFIG {
  DWORD dwServiceType;
  DWORD dwStartType;
  DWORD dwErrorControl;
  LPTSTR lpBinaryPathName;
  LPTSTR lpLoadOrderGroup;
  DWORD dwTagId;
  LPTSTR lpDependencies;
  LPTSTR lpServiceStartName;
  LPTSTR lpDisplayName;
} QUERY_SERVICE_CONFIG, 
*LPQUERY_SERVICE_CONFIG;

となっています。VBA(VB6)とはまったく違うので戸惑うかもしれませんが、 注目してほしいのはVBA(VB6)でString型で定義したメンバの型はC言語ではLPTSTRという型で 定義されているということです。この型はANSIとUNICODEの文字コードの違いを吸収するために このようになっているのですが、要はchar型、または、wchar_t型へのポインタです。 これまた分からない型かと思いますので以下をご覧ください。

23番地

I L i k e V B A \0

7番地

23番地

これがポインタ(名前がlpBinaryPathName)

上記はコンピュータのメモリを模式的に描いたもので1マスが1バイトとします (ポインタは4バイト)。コンピュータはメモリをそのアドレス(住所のようなもの)として 1バイトずつ管理しています。このアドレスを頼りに様々な処理をします。
ポインタは変数です。変数というと何かを入れる器ですが、例えばVBAのLong型は整数を入れる 4バイトの器です。ではポインタは何を入れる器かというと、上記の図からわかるようにメモリの アドレス(住所)を入れる器です。アドレスをいれますが、上記の例で23番地という情報だけでは そのアドレスにどんな情報が入っているかわからないので「char型への」と型情報が付加されます。

文字列は23番地から始まることがわかりますが、どこで終わるかわかりません。 その終わりを示すのがNULL文字(\0)です。ですから、型と始まりの番地さえ分かれば文字列が 取得できるのです。

QUERY_SERVICE_CONFIG のlpBinaryPathNameを例にすると、7番地の変数名が「lpBinaryPathName」で、この変数が指し示す 23番地以降NULL文字までに文字列が入っているということになります。

話を元に戻しますが、 QUERY_SERVICE_CONFIG 型の結果を受け入れるためのサイズ は23番地以降のメモリを含めたサイズであり、 QueryServiceConfig の第四引数で受け取るサイズでもあります。一方、 QUERY_SERVICE_CONFIG型のサイズ は文字列がどんなに長くなっても(23番地以降のメモリがどんなに増えても)7番地に入っている ポインタの値は「23番地」で4バイト(32ビット)固定です。ということで QUERY_SERVICE_CONFIG型のサイズ は一定ということになります。

・QueryServiceConfigのDeclare宣言の変更

QueryServiceConfig の第三引数に指定する、サービス構成の結果を受け取るためのサイズは 先ほど書いたように不定(わからない)ので、まず、第二、第三引数に小さな値を設定してわざと エラーを起こし、第四引数に必要サイズを取得する必要があります。そして再度、取得したサイズを QueryServiceConfig の第三引数に指定して、第二引数に結果を受け取ります。

ところが、第二引数の型は QUERY_SERVICE_CONFIG 型、正確にはQUERY_SERVICE_CONFIGへのポインタ(QUERY_SERVICE_CONFIGのインスタンスの先頭アドレス)です。 これは、C言語での QueryServiceConfig の宣言を見ればわかります。

BOOL QueryServiceConfig(
  SC_HANDLE                hService,                // サービスのハンドル
  LPQUERY_SERVICE_CONFIG lpServiceConfig,         // バッファ
  DWORD                    cbBufSize,               // バッファのサイズ
  LPDWORD                  pcbBytesNeeded           // 必要なバイト数
);

前項でQUERY_SERVICE_CONFIG型のサイズ は一定で実際に文字列が格納されている メモリサイズが考慮されていませんと書きました。つまり、

Dim vtSvcConf As QUERY_SERVICE_CONFIG
QueryServiceConfig(xxxx, vtSvcConf, xxxx, xxxx)

とした場合、vtSvcConfのString型のメンバは、実際には文字列か格納されている領域の 先頭アドレスだけが格納されているのですが、そのアドレス先に文字列を入れるだけの領域が 確保されていないということです。それでは、

Dim vtSvcConf As QUERY_SERVICE_CONFIG
With vtSvcConf
    .lpBinaryPathName = Space(500)         
    .lpDisplayName = Space(500)
    
            ・
            ・
End With
QueryServiceConfig(xxxx, vtSvcConf, xxxx, xxxx)

このように文字列の領域を確保しておけばいいじゃないかという話になります。 しかしこれもうまくいきません。QUERY_SERVICE_CONFIG型のサイズ分の 確保された領域(vtSvcConf)とSpace関数で確保された文字列を格納するための領域がバラバラで 連続しているとは限らないからです。Space関数を発行した時点でVBAは、メモリ上のどこかに5 00の領域をつくってその先頭アドレスをlpBinaryPathNameなどにいれているのですが、 その新たに作った領域はどこかわからないですし、vtSvcConfの領域につづいて作られるわけではないのです。 QueryServiceConfig の第二引数で要求しているのは連続した 結果を受け入れるためのサイズの 先頭アドレス(ポインタ)です。

ではどうするかというと第二引数の型をByte型として宣言し直します。最初の QueryServiceConfig の第四引数で受け取った必要バイト数分のByte型の配列を確保してそのポインタを第二引数 に設定してやります。こうしてやれば、連続した領域へのポインタを QueryServiceConfig の第二引数に指定できます。

Private Declare Function QueryServiceConfig Lib "advapi32.dll" Alias _
                                                      "QueryServiceConfigA" _
       (ByVal hService As Long, _
        lpServiceConfig As Byte, _
        ByVal cbBufSize As Long, _
        pcbBytesNeeded As Long) As Long

ところでVBA(VB6)でどうやってポインタを指定するの?と感じるかもしれませんが、 知らない間にポインタを設定しているんです。それが参照渡しです。

Sub main()
    Dim b As Byte

    b = 1               '(A)
    func(b)             '(B)

    Debug.Print b       '(C)
End Sub

Sub func(arg As Byte)   '(D)
    arg = arg + 1
End Sub

(A)で変数bに1を代入して、(B)でbを渡しています。ここでVBA(VB6)はデフォルトで 参照渡しなので、funcの引数は1という値を受け取っているのではなく1という値が格納されている 変数bのアドレスを渡しています。もし、1という値が渡っているのでしたら(C)は1が出力されるはず ですが、実際には2になります。正確にはポインタと参照は違うのですがその説明は割愛します。

ですから、例えば、結果を受け取る必要サイズが357バイトだとすると

Dim b(356) As Byte
QueryServiceConfig(xxxx, b(0), 357, xxxx)

とすればByte型配列の先頭アドレスが渡って結果をbの配列に受け取ることができます。

・QUERY_SERVIC_CONFIG構造体のメンバの型の変更

受け取った結果はByte型の配列です。これを QUERY_SERVICE_CONFIG構造体 のインスタンスに代入しないといけないのですが、その前にどのようにデータが格納されているかを説明します。

便宜上、 QUERY_SERVICE_CONFIG のメンバをdwStartType、lpBinaryPathName、dwServiceType、lpDisplayNameの4つのみと仮定します。

Dim b(必要サイズ数) As Byte

として QueryServiceConfig で結果を受け取ったメモリの状態が上図のような感じなります。赤枠の 部分がQUERY_SERVICE_CONFIG型のサイズになります。

この図からメモリ操作で先頭の16バイトを QUERY_SERVICE_CONFIG構造体 のインスタンスにコピーしてあげれば、 QUERY_SERVICE_CONFIG の情報として取り出せます。このメモリ操作はAPIの CopyMemoryを使います。
しかし、lpBinaryPathNameとlpDisplayNameはアドレスが入っているだけです。アドレスは32ビットの 正の整数なので、これをString型としてVBAで定義すると値が受け取れないので、String型はLong として定義しなおします。

Private Type QUERY_SERVICE_CONFIG
    dwServiceType      As Long
    dwStartType        As Long
    dwErrorControl     As Long
    lpBinaryPathName   As Long     'String
    lpLoadOrderGroup   As Long     'String
    dwTagId            As Long
    lpDependencies     As Long     'String
    lpServiceStartName As Long     'String
    lpDisplayName      As Long     'String
End Type

でもLong型でアドレスを受け取ってもそのアドレスが指し示す実際のデータはどうやって取り出すの? という疑問がわいてくるかと思います。これはC言語のstrcpyという関数のAPI版があるのでそれを 使わせてもらいます。(だだし、今回必要なのはLong型のdwStartTypeなのでおまけとして後ほど 書きたいと思います)

・スタートアップの種類の取得の前準備 2

実装前の最後の準備として QueryServiceConfigQUERY_SERVICE_CONFIG構造体 以外の必要な関数・定数の宣言をします。

ここ でわざとエラーを起こしたときにそのエラーコードを識別するための 定数ERROR_INSUFFICIENT_BUFFERの定義をします。

MSDNの QUERY_SERVICE_CONFIG構造体 のdwStartTypeメンバの解説にある設定できる定数一覧を定義します。( Win32Api.txtにはないです)

OpenServiceの第三引数に指定する定数は QueryServiceConfig 関数が使えるようにする定数を設定しないといけないのでそれを定義します。

ここ で必要なメモリ操作のためにCopyMemoryを宣言します。

では Win32Api.txtからコピーしましょう。

'サイズ不足失敗時エラーコード
Private Const ERROR_INSUFFICIENT_BUFFER = 122     'dderror

'スタートアップの種類の定数
Public Const SERVICE_AUTO_START   As Long = &H2   'win32api.txtになし
Public Const SERVICE_DEMAND_START As Long = &H3   'win32api.txtになし
Public Const SERVICE_DISABLED     As Long = &H4   'win32api.txtになし
'Public Const SERVICE_NONE         As Long = &H0   '既に定義済み  

'OpenService用
Private Const SERVICE_QUERY_CONFIG = &H1

'メモリコピー
Private Declare Sub CopyMemory Lib "kernel32" Alias "RtlMoveMemory" _         
          (Destination As Any, _
           Source As Any, _
           ByVal Length As Long)
      

ずいぶん前置きが長くなりましたが、ここまでが理解できないとこの次に記述する VBAのコードの意味がまったく理解できないはずです。ということで次回はいよいよスタートアップの 種類の取得のための実装を書いていきたいと思います。