リストHOME  リストOpen Source

第4回VBAプログラム豆知識

概要 ~ WinAPI使用の注意点 ~

 今回は、VBAでWindows API(Application Programming Interface)を利用する場合の注意点について、説明したいと思います。APIは、ほとんどがC言語により、開発されているため、関数を使用する時には、少なからずC言語の知識が必要になってきます。特に、APIを使用するための宣言文(Declare文)を記述する際には、C言語とVBAのデータ型の違いに気をつけなければなりません。


 よく間違える可能性があるのは、以下の2点ではないでしょうか。



 これをもう少し詳しく説明します。

 C言語のint型は、プラットフォームによって、サイズが異なります。2バイトと4バイトの可能性があります。16ビットコンピュータでは、サイズが2バイトなのですが、32ビットコンピュータ以降は、4バイトとなっています。現在では、ほとんど16ビットコンピュータを目にすることがなくなってしまったので、int型は、4バイトと思ってください。以上のことから、C言語のint型は、VBAのLong型に置き換えます。


 C言語のBOOL型をVBAのBoolean型に置き換えてはいけません。それは、C言語のBOOL型は4バイト、VBAのBoolean型は2バイトと、サイズが異なるなるからです。BOOL型の定義は、Falseは数値の0、Trueは0以外の数値となっています。従って、BOOL型といっても、ただの数値なのです。ですので、C言語のBOOL型は、VBAのLong型に置き換えてください。


 私は、元システムエンジニアなのですが、当時、制御系のエンジニアでした。そのため、C言語の経験がほとんどだったので、てっきりVBAのBoolean型も4バイトと思い込んでしまった訳です。Boolean型のサイズを誤認したまま、APIを使用したため、後に、プログラムが正常に動作しなくなったという経験があります。


 このミスを具体的に例を使って説明したいと思います。以下のように、Kernel32.dllに、APIのDataEncodeという名前のエクスポート関数があったとします(架空)。処理内容は、入力データを暗号化するという内容とします。
※エクスポートとは、関数を外部に公開しているという意味です。逆に公開しているAPIを呼んだ側は、関数をインポートしたという表現を使います。


// 説明のために創作した架空のAPIです。
// Windows API : kernel32.dll 

// データ変換処理API
// *** 引数 ***
// inp:入力バッファのアドレス
// size:入力バッファのサイズ
// flg:変換処理分岐フラグ
// out:出力バッファ
// *** 戻り値 ***
// 出力バッファのサイズ
long DataEncode(long inp, long size, BOOL flg, long out)
{
    if (flg) {
        /* データを暗号化する処理が、
         C言語にて、記述されていたとします。
         
         *** 処理1 ***
        
        */

    } else {
        /* データを暗号化する処理が、
         C言語にて、記述されていたとします。
         
         *** 処理2 ***
        
        */
    }

    return out_size;

}


 次に、下記が、上記で記述したDataEncode関数を利用するためのVBAの宣言文と、そのAPIを使用した実装例となっています。ここで、注目してもらいたいのが、C言語のBOOL型の引数flgが、VBAの宣言文で誤って、Boolean型に置き換えられている点です。先ほども、説明しましたが、C言語のBOOL型と、VBAのBoolean型はサイズが異なります。


'*** WinAPI 宣言文 ***

'上記のAPIを使用するためVBAに記述した宣言文
Public Declare Function DllDataEncode Lib "kernel32.dll" Alias "DataEncode" _
                                            (ByVal inp As Long, _
                                            ByVal size As Long, _
                                            ByVal flg As Boolean, _
                                            ByVal out as Long) As Long

'*** 実装 ***

'データの暗号化
Private Function ConverDataBuffer() as Long
    Dim inp(INP_SIZE) As Byte
    Dim out(2 * INP_SIZE) As Byte
    Dim out_size As Long
    Dim i As Long

    '入力バッファ 0,1,2,3 …
    For i = LBound(inp) To UBound(inp)
        inp(i) = i

    Next i

    '変換処理
    out_size = DllDataEncode( _
                                    VarPtr(inp(Lbound(inp))), _
                                    UBound(inp) - LBound(inp) + 1, _
                                    True, _
                                    VarPtr(out(LBound(out))))

    ConverDataBuffer = out_size

End Function




 ConverDataBuffer関数から、APIのDllDataEncode関数を呼んだときの、スタックの動きを図にしたものが下記です。APIの呼び出し側となるVBAは、関数の引数を右から順番にスタックに積んでいきます。

スタック参照

※SPはスタックポインタ(スタックの現在位置)、Bはサイズの単位【Bytes】です。


 VBA側からDllDataEncodeを呼び出すときには、スタックには、引数が右側から、out(4B), flg(2B), size(4B), inp(4B)の順番に積まれます。呼ばれた側は、out(4B), flg(4B), size(4B), inp(4B)と思って、スタックに積まれた引数を参照します。ここに矛盾が生じているのが、分かるかと思います。flgのサイズが、呼び出し側と、呼ばれた側で異なる為、inpとsizeの値は、正しい値がAPI側に渡されますが、flgとoutは、意図している値とは、異なる値として、API側が参照してしまいます。


 誤った値を参照してしまうのは当然ですが、特に問題があるのが、引数outの値を誤って誤認してしまっている点です。これによりAPIが、呼び出し側の意図しているアドレスとは、まったく異なるアドレスにメモリ参照して、値を変更してしまい、これが、プログラムに異常をもたらす原因となってしまいます。


 もしかするとoutの値が、スタック領域に該当し、スタックの値を変更してしまっているかもしれません。前にもお話ししましたが、スタックには関数の戻り先(アドレス)が保存されています。その戻り先の値を変更してしまっていた場合、プログラムが呼び出した位置ではなく、まったく異なる位置に戻ってしまう可能性があります。これは、関数Aから関数Bを呼び出したにも、関わらず、関数Bの処理終了後、関数Cの位置に戻ってしまう可能性があると言うことです。


 私は、Booleanのサイズを誤認していたため、この動きが実際に発生してしまいました。そのとき初めて、スタックに異常が発生していることに気づき、APIの宣言文を確認し、ようやく、不具合を改善することができました。


 このような、APIの宣言文の記述ミスは、NULLアクセスなどの、メモリアクセス違反が起こる可能性非常に高くなります。この場合、エクセルが強制終了させられてしまいます。開発中にエクセルが強制終了するような場合は、APIの宣言文を疑ってみてください。


 もし、APIを使っていなければ、基本的に、このような強制終了は発生せず。異常発生直前にソフトウェア割り込みが発生し、エクセル(VBA)が、異常通知の画面(スタックのオーバーフローや、配列のインデックスオーバー)を表示してくれます。すなわち、エクセルが、強制終了という最悪の事態を回避してくれているのです。
 なのですが、APIを使ってしまうと、呼ばれた先が、エクセルの管理下から外れてしまい、このような異常画面が表示されず、最悪、強制終了という結果になってしまいます。
※管理下という表現ですが、これは推測ですが、エクセルの機能制限ではないかと思っています。通常の開発環境では、DLL内のアセンブラ・ソースが表示されデバッグする流れとなります。


 安定したプログラムを書きたいというのであれば、少し消極的ですが、APIを使用しないというのも一つの手だと思います。APIを使用しなければ出来ないことがあった時だけ、APIを使用することをお勧めします。


 私のミスは、Boolean型のサイズを誤認していたことが原因なのですが、最初は、プログラムを疑って、デバッグ、コードレビューしていたのですが、どうもバグの箇所の特定が出来ず、原因がなかなか突き止められませんでした。しばらくして、プログラムの動作が不安定で、とんでもない位置に、プログラムがジャンプしていたことで、スタックの異常と気づき、その際に、初めて、APIの宣言に問題があるのではと、疑いデバッグしたことで、ようやく、不具合を改善することが出来ました。
 まさか、宣言に問題があるとは、思ってもみませんでした。皆さん、プログラムが、正常に動作しない場合、それは、書いたプログラムのどこかに、必ず原因があります。慎重に、デバッグしましょう。



第3回  <  >  第5回



連絡先

 ご意見・ご要望等ありましたら、画面最下部のメールアドレスまでご連絡ください。




エクセルバックアップ・ページのフッター
管理者のメールアドレス