Programming Field - プログラミング Tips

VB6.0 で wvsprintf を使ってみる

※ 現在wvsprintf関数は(バッファーオーバーフローの可能性をはらむため)使用してはならない関数とされています。本ページの内容は「va_list」型のVBでの扱い方を紹介しているという観点でご覧ください。

まず最初は、VB 6.0 や VBA 6.0 (Excel マクロなどのこと) 上で Windows の関数「wvsprintf」を使ってみたコードを載せてみます。

※ 以下のコードは、Intel 系の 32 ビットプロセッサを想定しています。それ以外の場合は上手く動かない可能性があります。

wvsprintf の C 言語での定義は以下の通りです。

[C/C++]

int WINAPI wvsprintf(LPTSTR lpOutput, LPCTSTR lpFormat,
    va_list arglist);

この関数は、lpFormat で指定した書式と引数のリストを使って、lpOutput に文字列を出力する関数です。wsprintf が可変個の引数を取ることに対して、wvsprintf は va_list 型を使って「可変個の引数のリスト」を代わりに引数として受けています。つまり、va_list 型の引数リストが既にある場合は、wvsprintf を使えばよいということになります。逆に、va_list 型がコードに出てくるのはほとんど無いので、普通は wsprintf を使用して文字列を生成します。

ところが VB の場合は、Declare ステートメントで呼び出すことの出来る関数は、「Stdcall 呼び出し規約」で定義された関数に限られています。wsprintf 関数は「Cdecl 呼び出し規約」で書かれた関数なので、Declare で定義することは出来ません。

そこで wvsprintf 関数の定義を試みますが、これを VB のコードに移行しようとすると、太字で示した部分がネックになってきます。VB には「va_list 型」に対応するもの自体が存在しないためです。

ただ va_list 型はポインタであるため、便宜上次のように定義できます。

[VB 6.0]

Declare Function wvsprintf Lib "user32.dll" Alias "wvsprintfA" _
    (ByVal lpOutput As String, ByVal lpFormat As String, _
    ByVal arglist As Long) As Long

そして、引数 arglist にポインタとして与えるべきデータ(引数リスト)を自分で作れば、この関数を使うことが出来ます。

問題は arglist に与えるべきデータがどういった構造になっているかですが、このデータは意外と単純で、「ポインタ(32 ビット値)の配列」です。引数リストに含まれる引数が数値ならそのまま、文字列ならそのポインタが入ります。例えば引数リストが 12, "あああ" となっていたら、配列は 0x0000000C、0xSSSSSSSS (0xSSSSSSSS は "あああ" を指す文字列のポインタ) となります。

※ Single(float) 型の場合は、それ自身が 32 ビットの値なので、それを 32 ビットの整数値に変換した値をセットします。Double 型の場合は 64 ビット値なので、2 つの 32 ビット整数値に分け、低い方、高い方の順で 2 つを連続してセットします。

これで arglist 引数を渡すことが出来ます。注意してほしいのは、wvsprintfA 関数を呼び出す場合は文字列をすべて Unicode 文字列から変換する必要があるということです。関数の引数に直接文字列を渡す場合は自動的に変換してくれますが、そうでない場合は組み込み関数(VBA 名前空間)の StrConv を使って変換する必要があります。

以上をコードにしたものを下に載せます。

このコードは、VB 6.0 または VBA 6.0 で使用できます。

※ このコードは、簡単のためバッファオーバーランなどのチェックを一切行っていないため、セキュリティ的問題があります。使う場合は自己責任でお願いします。

[VB 6.0]

Private Declare Sub CopyMemory Lib "kernel32.dll" Alias "RtlMoveMemory" _
    (ByRef Destination As Any, ByRef Source As Any, _
    ByVal Length As Long)
Private Declare Function wvsprintf Lib "user32.dll" Alias "wvsprintfA" _
    (ByVal lpOutput As String, ByVal lpFormat As String, _
    ByVal arglist As Long) As Long

' Double 型を 2 つの 32 ビット型整数に分ける処理
Private Sub SplitDoubleToTwoLong(ByVal d As Double, _
    ByRef LoDWord As Long, ByRef HiDWord As Long)
    Dim l(0 To 1) As Long
    Call CopyMemory(l(0), d, 8)
    LoDWord = l(0)
    HiDWord = l(1)
End Sub

' Single (float) 型を 1 つの 32 ビット型整数に変換する処理
Private Function FloatToLong(ByVal f As Single) As Long
    Call CopyMemory(FloatToLong, f, 4)
End Function

' MyPrintF - VB 版 printf 関数
'   Format: 書式を含む文字列
'   Arguments(): 書式指定子によって置き換えるデータ(可変数)
'   戻り値: 変換した文字列
Public Function MyPrintF(ByVal Format As String, _
    ParamArray Arguments() As Variant) As String
    Dim Buffer As String, i As Long, c As Long, v As Variant
    Dim Pointers() As Long, Strings() As String
    ' 文字列のバッファを確保するための長さを計算
    i = LenB(Format)
    c = 0
    For Each v In Arguments
        ' 便宜上の処理 - 本当は書式指定子などで長さは異なる
        i = i + LenB(CStr(v)) + 3
        c = c + 1
    Next v
    i = i + 20
    ' 文字列のバッファを作成
    Buffer = String$(i, 0)
    If c > 0 Then
        ' arglist に当たる引数リストの作成
        ' Strings 配列は ANSI 文字列が破棄されない
        ' ようにするための配列(念のため)
        ReDim Pointers(0 To c - 1), Strings(0 To c - 1)
        c = 0
        For Each v In Arguments
            Select Case VarType(v)
                Case vbString
                    Strings(c) = StrConv(v, vbFromUnicode)
                    Pointers(c) = StrPtr(Strings(c))
                Case vbSingle
                    Pointers(c) = FloatToLong(CSng(v))
                Case vbDouble
                    Call SplitDoubleToTwoLong(CDbl(v), _
                        Pointers(c), Pointers(c + 1))
                    c = c + 1
                Case Else
                    Pointers(c) = CLng(v)
            End Select
            c = c + 1
        Next v
    Else
        ' 引数リストは無いが、有効なポインタを得るため
        ' 長さ 1 の配列を作っておく
        ReDim Pointers(0)
        Pointers(0) = 0
    End If
    ' 引数リストが出来たのでそのポインタを渡す
    Call wvsprintf(Buffer, Format, VarPtr(Pointers(0)))
    ' NULL 文字の消去
    i = InStr(Buffer, Chr$(0))
    MyPrintF = Left$(Buffer, i - 1)
End Function

最終更新日: 2007/10/13