えくせるちゅんちゅん

ことりがエクセルをちゅんちゅんするブログ

WinAPIの64bit化で出てくるPtrSafe、LongLong、LongPtrってなんなのさ?

WinAPIを64bitに対応しようとした時に絶対に覚えなくてはならないのがLongPtrである。

Declare文の64bit対応に関しては以前次のような記事を書いたが、LongPtrの置き換えの考え方に関しては一切説明できなかった。

今回は64bit対応のためにPtrSafe/LongPtrが実装された経緯について、私が想像したことを説明する。

あくまで個人的に「たぶんこうだったんだろうな」と理解したまでで、公式に得た情報ではないので注意するように。

www.excel-chunchun.com


何故PtrSafeが生まれたのか

64bit環境では、PtrSafeを書かないと無条件にコンパイルエラーとなる。

例えば、あるウィンドウをアクティブに切り替えるAPISetForegroundWindowのDeclare文は次のようになる。

#If VBA7 Then
    '32bit / 64bit対応 ※Excel2010以降限定
    Declare PtrSafe Function SetForegroundWindow Lib "User32" (ByVal hWnd As LongPtr) As Long
#Else
    '32bit限定
    Declare Function SetForegroundWindow Lib "User32" (ByVal hWnd As Long) As Long
#End If


多くのVBAではコンパイルエラーがあっても実際に流入する瞬間までエラーは出ないのだが、宣言セクションなどの全体に影響する部分のコンパイルエラーがある時は、一切のVBAの実行ができなくなるように出来ている。

詳しくは後ほど説明するが、LongPtrへの置き換えを行わなければVBA実行中にExcel自体がクラッシュすることになるので「ちゃんとチェックした」ということをコンパイラに事前に伝えるために生まれたのが「PtrSafe」である。

64bit環境では、一つでもPtrSafeが不足しているとプロジェクト全体が64bit非対応とみなし、そのAPIを使用するか否かに関わらず一切のプロシージャの実行を認めないというかなり厳しい制限を敷いている。

もし実行できてしまったらExcelがクラッシュすることになるので、そのくらい厳しい制限で丁度よいと私は考えている。


というわけで、「PtrSafeを書く」たったそれだけで「コンパイルエラーは出なくなる」

だが、本来すべきことはソコじゃない。

  • 引数の型をLongPtrに変える
  • Typeメンバの型をLongからLongPtrに変える
  • API関数に渡すデータ・変数の型をLongPtrに変える
  • 戻り値の型をLongPtrに変える

このように、仕様を調べて適切なデータ型に変えさせるのが真の目的なのである。

元々の型がLongとは限らなくて、StringやAnyなどからLongPtrに置き換えることもあるが、ややっこしいので説明は省略する。とにかくメモリ・ポインタ・ハンドル関係は間違いなく対象となる。

PtrSafeを付けるのは「ちゃんとチェックしたよ!!!」と宣言するために書くべきなのだが、現実は「とりあえず書いとけ」状態になっている。コンパイルが通らないことには始まらないので、型の変更が後回しになるのは仕方がないのだが・・・。


LongLongとは一体何なのか

LongPtrの話をする前に、64bitで増えたLongLongが何者なのか知らなければならない。

昭和生まれの方なら、32bit時代のメモリ制限には苦しめられた人が少なくないのではないだろうか?(尤も、私は平成生まれだが・・・)

32bit OS(XP)ではメモリを3GB強しか認識できなかった。2GBx2本を搭載して4GBにしても、実際に使えるのは3.3GBとかその程度で残りは使用できなかった。

32bitアプリでは1つのプロセスで2GBまでしか使用できず、メモリの食う画像処理では重い足かせとなっていた。

これは64bit OSでも同じことで、32bitアプリを動かす時は2GB制限がかかっている。動画編集ソフトaviutlなどで有名だ。高負荷処理を行うアプリは早々に64bit化されており、ついに一般向けのExcelにまで降りてきているのが現状である。

さて、OSかアプリかの違いはあるものの、64bitに変化したことで得られるメリットは、このメモリ制限の撤廃だ。64bit化されたことで、今後数百年は使い切れないほど無限のメモリを使う事ができる。


なぜ使えるメモリの容量が増えたのかと言えば、メモリアドレスの桁数が増えたからなのだ。

32bit環境では2進数で32桁までのメモリアドレスしか格納できなかった。2の32乗=4,294,967,296=4GiBである。

「アレヤコレヤで4GBから更に削られて、3GBの壁や2GBの壁があった」というイメージで、私はなんとなく理解している。

従来の32bit VBAでは、このメモリアドレスを格納するデータ型としてLong型を使用してきた。


ところが64bit環境では、2進数で64桁まで格納できるようになり、もはや使い切れないくらい巨大な空間にアドレスを振れるようになった。

それはつまり桁数が2倍に増えているので、変数の大きさも2倍にしないといけないということを意味する。

当然、Long型には入り切らない。

だから、64bit環境のメモリアドレスが格納できるように、Long型の2倍のサイズを持つLongLong型が出来た。


何故LongPtrが生まれたのか

だが、実際にLongLongを見かけることはめったに無い。

APIのサンプルをみると、何処を見てもLongPtrだと思う。

なぜなら、その方が都合が良いからである。

環境に応じてデータ型変化するLongPtrがあると、コードが非常にスッキリするのだ。


例えば、https://docs.microsoft.com/ja-jp/windows/win32/api/winuser/nf-winuser-setforegroundwindow によれば、SetForegroundWindowは次のように規程されている。

BOOL SetForegroundWindow(
  HWND hWnd
);
  • 引数 hWnd : ウィンドウハンドル
  • 戻り値 : 失敗時0、成功時0以外の値


これをLongPtrを使わずに書くと、このようになってしまう。

#If Win64 Then
    '64bit限定 ※Excel2010以降限定
    Declare PtrSafe Function SetForegroundWindow Lib "User32" (ByVal hWnd As LongLong) As Long
#ElseIf VBA7 Then
    '32bit限定 ※Excel2010以降限定
    Declare PtrSafe Function SetForegroundWindow Lib "User32" (ByVal hWnd As Long) As Long
#Else
    '32bit限定 ※Excel2007以前対応
    Declare Function SetForegroundWindow Lib "User32" (ByVal hWnd As Long) As Long
#End If

(LongLongが無い)32bit環境のためにWin64で分岐し、(PtrSafe・LongPtrが無い)Excel2007以前のためにVBA7で分岐しなければならない。

あまりにも冗長すぎる。

結果として1つの関数のために3回同じようなことを書く羽目になっている。

3つ目は古いExcel用のため切り捨てられるとしても、最初の2つは書かないといけない。

これが未来永劫続くを防ぐために、MicrosoftはLongPtrを実装したのではないかと考られる。


LongPtrはExcelの環境が32bitか64bitかで、実体の型が変化するという特別なデータ型である。

  • 32bit環境ではLongになる。
  • 64bit環境ではLongLongになる。

つまり、LongPtrを使えば2行を1行にできる。

Declare PtrSafe Function SetForegroundWindow Lib "User32" (ByVal hWnd As LongPtr) As Long

これは素晴らしい発明だ!


今後どうしたら良いのか

困ったことにLongPtrが原因で本質を理解しずらくなってしまっている。

Declare文の変更は良い。Win32API_ptrsafe.txtをコピペするなり、仕様書を読めば置き換えるのにさほど苦労はしないからだ。

だが、影響範囲はDeclare文だけに収まらないのだ。


APIを使う側」も修正しなければならない。

Sub Test_SetForegroundWindow()
    Dim ActiveHWnd As Long 'LongPtrが正しい
    ActiveHWnd = Application.hWnd
    SetForegroundWindow ActiveHWnd
End Sub

この例くらいなら暗黙の型変換で問題なく動作する可能性が高いが、場合によってはコンパイルエラーが出たりExcelが瞬時にクラッシュしてしまう。


関数の戻り値を渡す場合にも注意しなければならない。

Sub Test_SetForegroundWindow2()
    SetForegroundWindow GetHWnd
End Sub

Function GetHWnd() As Long 'LongPtrが正しい
    GetHWnd = Application.hWnd
End Function


API関数の戻り値がLongPtrとなる場合も、代入時に修正しなければコンパイルエラーー「型が一致しません」が出る。

Private Declare PtrSafe Function GetActiveWindow Lib "User32" () As LongPtr

Sub Test_GetActiveWindow()
        Dim n As Long 'LongPtrが正しい
    n = GetActiveWindow()
End Sub


API以外でもコンパイルエラー「型が一致しません」が出ることはある。

Sub Test_VarPtr()
    Dim n As Long 'LongPtrが正しい
    n = VarPtr(n)
End Sub


特に恐ろしいのはVarPtrで変数のポインタを渡す場面だ。コンパイル時点では検知不可能なので、VBA実行中にExcelがクラッシュする。

LongLongのポインタを渡すべきところでLongのポインタを渡したら、API内部でLong以上のブロックのメモリに書き込むことになるためメモリが破壊されてしまう。

だからStrPtrとかVarPtrとかで渡しているところは全部チェックしないといけない。


よくある失敗

こうしてみると、Longは全部LongPtrで良いようなしてくるが、そんなことはない。

LongをLongLongに入れることはできるが、LongLongをLongに入れるのは不可能だ。

つまり、Long型引数を持つAPIにLongPtrで宣言した変数を渡すとコンパイルエラーが出る。


Typeのメンバの型も注意が必要だ。

API側で書き込む構造体の大きさは明確に決められているので、LongにすべきところをLongLongにしてしまったら大きさが仕様と変わってしまう。

当然、そのような構造体を渡したらクラッシュする。

だから何でもかんでもLongPtrにすれば良いというものではない。


そして最悪の失敗は、32bit環境で64bit対応に挑戦することである。

これは断言できる。対応は不可能だ。

32bit環境でLongPtrはLongとして動作する。つまり全部LongPtrに変えてもエラーが出るわけがないのだ

エラーが出ないのに64bit対応できるわけがない。

面倒かつ財布に厳しい話ではあるが、APIを使う人は最低でも64bit環境で開発するか、両方を保有してテストを通す必要がある。


参考資料

以前、こんな連続ツイートをした。

https://twitter.com/KotorinChunChun/status/1274154632244166656?s=20

Win64APIは文字通り64bit用のAPIなんだけど、ほぼ全ての関数は32と共通のもので、引数や戻り値の型が64bit対応のために変化しているだけという印象。 特にメモリアドレスの空間が拡大された影響が大きくて、例えばVBAではポインタを示す箇所が全部LongからLongLongに変わっている。 だが・・

32bit/64bit両対応のために、そっくりなDeclare文を2回も書かないのはアホらしい・・という理由から、VBAでは LongPtr とかいう素晴らしい型が追加されており、ほぼ全てのWinAPIは書き直す必要が無くなった。 これが原因で、OSの仕組みの本質を理解出来ないで使う人が多くなっていると思う。

64bitでPtrSafeを書かされるのは、この「引数・戻り値の型」をちゃんと64bit対応のためにチェックしましたよ!というプログラマの意図をコンパイラに報告させる行為だと思う。 そうすることで、PtrSafeが無い文は64bit未対応という判定を、コンパイル時点でチェックでき、未対応のVBAは一切動かない。

もし「PtrSafeを書きなさい」というルールがなかったら、VBAは実際に関数を実行してメモリ破壊が起こるまで、間違っていることが分からない。 というか、ExcelまたはWindowsがクラッシュする。 実際に初学者が、本質を理解せずPtrSafeだけ追記してLongPtrに変えなかった場合に起こる現象そのまんま

かくゆう私も、ここまで理解するのに何百回もExcelをクラッシュさせたという経験がある。(トライアンドエラーの境地) それぞれのAPI(関数)をどう書き換えるかは、仕様書を読めば分かる。が、読むのは大変なので、Microsoft公式の Win32API_PtrSafe.txt からコピーせよ。

https://docs.microsoft.com/ja-jp/office/client-developer/shared/compatibility-between-the-32-bit-and-64-bit-versions-of-office

しかし、書かれていないAPIも結構たくさんある。

Win32API_PtrSafe.txt に書かれていない関数の宣言文がほしい時は、Google先生に聞いてみると良い。 例:"Declare PtrSafe Function GetDeviceCaps" ※ダブルクォーテーションで囲うのを忘れないこと。 PtrSafeのおかげで64bit対応版だけに絞って検索出来るので、とても都合が良い。

でも、ググって出てきた宣言文が仕様どおりに書かれている保証は無い。 とりあえずの参考には良いのだけど、ちゃんと複数バージョンでテストを通しておくとか、関数名で調べて仕様通りの型になっているかチェックしないとあぶない。


64bit対応とはズレるが、APIのASCII、UNICODE対応の関係。

CreateFileAをCreateFileWに置き換えるために、ちょっと苦労した話。

https://twitter.com/KotorinChunChun/status/1275591987022856193?s=20


以前、Declare文の置換ツールを開発した話。

www.excel-chunchun.com


以前、整数型の速度を比較した結果、64bitではLongLong(LongPtr)を使うのが一番高速という話。

www.excel-chunchun.com


まとめ

今日はLongPtrが何者か説明してみました。

私から言える事は一つ。

今後もAPIを使うなら、開発者のExcelは64bitにしておけ。

あとはじっくりとテストを行うしか無いと思う。

そうしないと配布してから苦しむことになる。

私のようにね。

現場からは以上です。


何か御座いましたらコメント欄、またはTwitterからどうぞ♪

それではまた来週♪ ちゅんちゅん(・8・)

プライバシーポリシー