LoginSignup
5
1

More than 1 year has passed since last update.

VBAの文字列変数とVarPtr,StrPtrについて、関係性を調べた。StrPtrとVB文字列型の正体について。

Last updated at Posted at 2022-02-13

最初に結論

  • VarPtrは 変数のアドレス
  • StrPtrは VarPtrが示すアドレス位置に格納されている値
  • StrPtrは VBがSysAllocString系の文字列データ割り当てをした結果、得られたアドレス値をVarPtrが示すアドレス位置にVBが保管したもの。

いきさつ

これを知って特に何かの役に立つわけではないですが、大昔、VarPtrとStrPtrの違いに興味をもって、自分でおこなった実験を、ここに記録してみたいと思います。
そして、よく言われる StrPtrと文字列の説明から、もう一歩踏み込んで、VBAの文字列変数の正体について、ここに記録を残したいと思います。

よく言われること

VarPtr は 変数のアドレス
StrPtr は 文字列のアドレス

疑問 : これは何を言ってるのか?

最初、これが何を言ってるのかよくわかりませんでした。
「文字列型変数は、文字列をもってるんだから、その変数のアドレスは、つまり文字列のアドレスなんじゃないか? だとしたら、VarPtrとStrPtrは一致するんじゃないのか?」 と思いました。

でも、VarPtrとStrPtrは一致せず、まったくの別の値となります。
このように実験できます。

Sub Main()
    Dim s As String
    s = "abc"
    Debug.Print Hex(VarPtr(s)), Hex(StrPtr(s))
End Sub

いま、手元で実行した結果は、このような結果になりました。 この結果は、そのときの状況に応じて変化しますので、あくまで一例です。
左が VarPtr , 右が StrPtr となります。 互いに異なる値です。

image.png

なぜ、二種類の~Ptr関数があるのでしょうか?
StrPtrだけあればよさそうに思えますが、なぜVarPtrもあるのでしょうか?

それは、StrPtr変数は、数値型変数の場合の数値に相当するものだからです。
あとで説明しますが、言い換えると、 VarPtrで得られるアドレスに格納されている値がStrPtrです。

数値型の場合には VarPtrで得られるアドレスには、1やら10やら数値型の値が入っていますが、文字列型の場合には VarPtrで得られるアドレスには、StrPtrで得られるアドレスが入っています。

たとえば 数値型変数に 1 や 10 を入れる場合を考えます。

Sub Main()
    Dim L As Long

    L = 1
    Debug.Print Hex(VarPtr(L)), L

    L = 10
    Debug.Print Hex(VarPtr(L)), L

End Sub

以下のような実行結果になりますが、Lの値が変更しても、LについてのVarPtrの値は一定です。

image.png

VarPtrは変数のアドレスです。つまり、コンピューターにとっては、変数そのものです。人間は変数名で変数を認識しますが、コンピュータは変数のアドレスが変数を認識しますので、内容にかかわらず、一定不変である必要があります。

このアドレスの場所に、1や10といった値を保存ことになります。

文字列型変数の場合も同じです。 中に入る文字列がどう変わろうとも、変数のアドレスは一定不変である必要があります。 StrPtrの値をもって変数のアドレスにしてしまうと、文字列の変更ができないという事態に陥ります。

たとえば、上の例で "ABC" を 0x228B7BC08E8 のメモリに格納して、0x228B7BC08E8をそのまま変数のアドレスとして扱うと、 "ABC"を"ABCD"に変更しようとしても、ABCの文字までは今までの場所に入るかもしれませんが、Dに当たるメモリの場所は、もうほかの変数によって使われているかもしれません。こうなると、文字列の変更ができません。

この状況を避けるために、VBは いったん適当な空いてる場所に新しく代入したい文字列を格納して、その適当な場所のアドレスを 変数に格納します。

では、その様子をみてみます。

Sub Main()
    Dim s As String

    s = "abc"
    Debug.Print Hex(VarPtr(s)), Hex(StrPtr(s))

    s = "abcd"
    Debug.Print Hex(VarPtr(s)), Hex(StrPtr(s))

End Sub

下のように、"ABC"のとき、"ABC"は 0x228B7CFAC08に格納され、"ABCD"のとき、"ABCD"は 0x228B7BC08EFという別の場所に格納されますが、いずれも VarPtrは 0x228BA972CF0 で不変であることが確認できます。

image.png

確認

では、 VarPtrの位置に StrPtr が入っているという上のほうの主張は、本当でしょうか?

CopyMemory と いうメモリアドレスの値をコピーできる Windows API を使って確認します。

まずは、CopyMemory APIの使い方を数値型変数で理解します。
第2引数でByVal指定で与えたアドレスに格納している値を第1引数でByRef指定した変数に転送することができます。その際に転送対象となるデータのバイト数を第3引数で指定します。
以下のコードは、変数Lのアドレス位置にあるデータを 変数Valueに転送し、表示します。

Private Declare PtrSafe Sub CopyMemory Lib "kernel32" Alias "RtlMoveMemory" (Destination As Any, Source As Any, ByVal Length As LongPtr)

Sub Main()
    Dim L As Long
    Dim Value As Long

    L = 1234

    'CopyMemory で メモリアドレスの値が取れることを整数型で試します。
    'Lのメモリアドレスに位置にある値を取得し、表示します。
    CopyMemory Value, ByVal VarPtr(L), LenB(Value)
    Debug.Print Hex(VarPtr(L)), Value

End Sub

結果はこのように代入した 1234 が表示されます。

image.png

これで、 CopyMemoryが指定アドレスのデータを取得するものであることが理解できました。

では、VarPtrで返されるメモリアドレスに StrPtrの値が格納されているか見てみます。

Private Declare PtrSafe Sub CopyMemory Lib "kernel32" Alias "RtlMoveMemory" (Destination As Any, Source As Any, ByVal Length As LongPtr)

Sub Main()
    Dim S As String
    Dim ValuePointer As LongPtr 'StrPtrの値は LongPtr型で Officeが32ビット版か64ビット版かでサイズが異なります。

    S = "ABCD"

    'Sのメモリアドレスに位置にある値を取得し、表示します。
    CopyMemory ValuePointer, ByVal VarPtr(S), LenB(ValuePointer)
    Debug.Print Hex(VarPtr(S)), Hex(ValuePointer)

    '比較のために StrPtrの値を表示します。
    Debug.Print Hex(StrPtr(S))

End Sub

この結果はこうなりました。

image.png

変数のアドレス位置 0x228BA972CF0 にあるデータは 0x228B7282848 でした。
そして、それは StrPtrが返す値 0x228B7282848 そのものでした。

ここで疑問

では、逆にいうと、 VarPtrが返すアドレスに格納されている値を無理やり書き換えれば、VBAの文字列変数を変更できるのか? という疑問がでてきませんか?

私には、そういう疑問がでてきました。
たとえば、こういうことです。
VBAの文字列は 1文字が2バイトで表されるUnicode文字の集まりなので、同じく2バイトであるInteger型の配列に文字列"ABC"の順に "A"の文字コード,"B"の文字コード,"C"の文字コードを代入して、この配列のアドレスを 文字列変数の VarPtrの位置に格納すれば、"ABC"という文字列として扱うのか? ということです。

コードでいうと、こういうコードになります。

下記コードをExcel VBAで実行すると、Excelが異常終了します。
もちろん Word VBAで実行すると、Wordが異常終了します。
Outlook, PowerPointも同様です。
試す場合は、必要なデータを予め保存しておいてください。

Private Declare PtrSafe Sub CopyMemory Lib "kernel32" Alias "RtlMoveMemory" (Destination As Any, Source As Any, ByVal Length As LongPtr)

Sub Main()
   '注意:このコードを実行すると、Excelが異常終了します。 Wordで実行した場合はWordが異常終了します。PowerPoint/Outllookも同じくです。

   Dim S As String
   Dim Characters(0 To 3) As Integer 'VBAのUnicode文字は2バイトで表現されていますので、2バイトであるInteget型の配列で文字列を表せないかという実験です。
   Characters(0) = AscW("A")
   Characters(1) = AscW("B")
   Characters(2) = AscW("C")
   Characters(3) = 0

   '文字列変数 S のアドレス位置に Characters変数の始まりのアドレスをデータとしていれる。
   Dim L As LongPtr
   L = VarPtr(Characters(0))      
   CopyMemory ByVal VarPtr(S), ByVal VarPtr(L), LenB(L)

   'SのStrPtrが Characters(0)のVarPtrと同じになったことを確認。
   'これで Sは "ABC"になったのか?
   Debug.Print StrPtr(S), VarPtr(Characters(0))
   Debug.Print S = "ABC"

   MsgBox S

End Sub

この結果はこうなります。

image.png

イミディエイトウィンドウに表示された StrPtrは Characters変数のアドレスと一致しますので、VBAをだまして、Characters変数の内容を文字列として扱うことに成功したようにみえます。
実際に、下記のように Sを表示した結果がABCと表示されているので、Characters変数の内容がSの内容として扱われているように見えます。

ただ、「S="ABC"」の比較結果は「False」です。ここが不思議です。

image.png

そして、そこから調べていくと、VBの文字列型は Microsoftのいうところの BSTR型であるということがわかってきます。

https://docs.microsoft.com/en-us/previous-versions/windows/desktop/automat/bstr によると BSTR型とは以下の内容です。

image.png

つまり、BSTR型とは、文字列のバイト長を示す4バイトの後に文字列のバイトが並び、最後に文字列終端を示す文字コード0のバイトで終わるデータです。

これにあわして、文字列長を示す4バイトを先頭に付けてみます。

Private Declare PtrSafe Sub CopyMemory Lib "kernel32" Alias "RtlMoveMemory" (Destination As Any, Source As Any, ByVal Length As LongPtr)

Sub Main()
   Dim S As String
   Dim Characters(0 To 5) As Integer 'VBAのUnicode文字は2バイトで表現されていますので、2バイトであるInteget型の配列で文字列を表せないかという実験です。
   Characters(0) = 6 'ABCは3文字6バイトです。0x00000006 の下位2バイトを入れます。
   'Windowsは メモリの格納が リトルエンディアン方式なので、下位バイトから格納します。
   Characters(1) = 0 'ABCは3文字6バイトです。0x00000006 の上位2バイトを入れます。
   Characters(2) = AscW("A")
   Characters(3) = AscW("B")
   Characters(4) = AscW("C")
   Characters(5) = 0


   '文字列変数 S のアドレス位置に Characters変数のABC部分の始まりのアドレスをデータとしていれる。
   Dim L As LongPtr
   L = VarPtr(Characters(2))  'Aの位置 = Characters(2)

   CopyMemory ByVal VarPtr(S), ByVal VarPtr(L), LenB(L)

   'SのStrPtrが Characters(2)のVarPtrと同じになったことを確認。
   'これで Sは "ABC"になったのか?
   Debug.Print StrPtr(S), VarPtr(Characters(2))
   Debug.Print S = "ABC"

   MsgBox S

End Sub

結果はこの通り、 S = "ABC" の比較結果が Trueになりました。
image.png

でも、Excelが異常終了することは変わりがありません。
これは、VBがBSTR文字列領域を確保して、確保した結果の先頭アドレスを StrPtrとして 文字列変数のアドレス位置に格納したのちに、使用し終わったBSTR文字列領域を解放する処理が自動的に処理される結果、VBがBSTRとして確保したわけではない Characters変数をBSTR文字列として解放しようとして、異常終了するようです。

では、このMain関数が終了する直前に StrPtrの値をクリアしておけば、すなわち文字列変数のアドレス位置に BSTR領域未確保を示す0をセットすれば、解放処理は起こらずに、異常終了は避けられるのでしょうか?
実験してみます。

Private Declare PtrSafe Sub CopyMemory Lib "kernel32" Alias "RtlMoveMemory" (Destination As Any, Source As Any, ByVal Length As LongPtr)

Sub Main()
   Dim S As String
   Dim Characters(0 To 5) As Integer 'VBAのUnicode文字は2バイトで表現されていますので、2バイトであるInteget型の配列で文字列を表せないかという実験です。
   Characters(0) = 6 'ABCは3文字6バイトです。0x00000006 の下位2バイトを入れます。
   'Windowsは メモリの格納が リトルエンディアン方式なので、下位バイトから格納します。
   Characters(1) = 0 'ABCは3文字6バイトです。0x00000006 の上位2バイトを入れます。
   Characters(2) = AscW("A")
   Characters(3) = AscW("B")
   Characters(4) = AscW("C")
   Characters(5) = 0


   '文字列変数 S のアドレス位置に Characters変数のABC部分の始まりのアドレスをデータとしていれる。
   Dim L As LongPtr
   L = VarPtr(Characters(2))  'Aの位置 = Characters(1)

   CopyMemory ByVal VarPtr(S), ByVal VarPtr(L), LenB(L)

   'SのStrPtrが Characters(2)のVarPtrと同じになったことを確認。
   'これで Sは "ABC"になったのか?
   Debug.Print StrPtr(S), VarPtr(Characters(2))
   Debug.Print S = "ABC"

   MsgBox S

   '文字列変数 S のアドレス位置に 0をデータとしていれる。
   'これで StrPtr(S)は0になる。
   Dim Zero As LongPtr
   Zero = 0

   CopyMemory ByVal VarPtr(S), ByVal VarPtr(Zero), LenB(Zero)
   Debug.Print StrPtr(S)

End Sub

イミディエイトウィンドウに最後に表示された 「0」が表す通り、文字列変数は未割当の状態に戻り、Main関数終了後にExcelが異常終了することもなくなりました。

image.png

何の役にも立たないが、もう一歩踏み込みたい。

冒頭でふれたとおり、これを知って、役立つことはほぼ何もないです。単に好奇心を満たすだけの行為です。ここで、この好奇心をさらに満たすためには、異常終了はしなくなりましたが、VBAがやっている文字列の割り当てをオウンコーディングでやってみたいと思います。
目指すことは、 上記のようなStrPtrのゼロクリアをしなくてもよい文字列変数を独自に割り当てることです。

いろいろ調べると、BSTRは SysAllocStringで割り当てられることがわかってきます。
SysAllocStringで割り当てられたアドレスを StrPtrとして格納してやるとよいということがわかってきます。

これを試してみます。

Private Declare PtrSafe Sub CopyMemory Lib "kernel32" Alias "RtlMoveMemory" (Destination As Any, Source As Any, ByVal Length As LongPtr)
Private Declare PtrSafe Function SysAllocString Lib "OleAut32.dll" (ByVal psz As LongPtr) As LongPtr

Sub Main()
   Dim S As String
   Dim Characters(0 To 3) As Integer 'VBAのUnicode文字は2バイトで表現されていますので、2バイトであるInteget型の配列で文字列を表せないかという実験です。
   Characters(0) = AscW("A")
   Characters(1) = AscW("B")
   Characters(2) = AscW("C")
   Characters(3) = 0 'SysAllocStringに渡すデータは 0で終わる必要があります。

   '文字列変数 S のアドレス位置に SysAllocStringで割り当てたアドレスをデータとしていれる。
   Dim L As LongPtr
   L = SysAllocString(VarPtr(Characters(0)))

   CopyMemory ByVal VarPtr(S), ByVal VarPtr(L), LenB(L)

   'SのStrPtrが SysAllocStringで割り当てたアドレスと同じになったことを確認。
   'これで Sは "ABC"になったのか?
   Debug.Print StrPtr(S), L
   Debug.Print S = "ABC"

   MsgBox S

   'ここで StrPtrのゼロクリアはせずに異常終了しないことを確認します。

End Sub

結果はこうなります。
StrPtr(S)の値は、SysAllocStringした結果のアドレスと一致し、MsgBoxでも ABCが表示され、Excelが異常終了することもありません。
文字列変数のVBの仕組みによらない独自の割り当てに成功しました。

image.png

SysAllocStringにより得られた文字列データ領域は 対となるSysFreeStringにより解放される必要がありますが、それはMain関数終了時に自動的に VBAによってされています。
したがって、自分でSysFreeStringをすると、二重解放となり、これも異常終了の原因になります。

結論

ここまで調べた結果、VBAの文字列型とは BSTR型に対するラッパー変数であるといえます。

これで役に立つこと

冒頭でふれたとおり、これを知って役に立つことはあまりないです。自己満足です。
もしあるとすれば、下記1点だと思います。

  • C言語でDLLを作成して、VBAから文字列変数のアドレスを渡して、C言語DLL側で VBAの文字列変数に SysAllocStringした文字列をセットできる。
5
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
5
1