Office VBA の WinAPIを64bitに対応しようとした時に絶対に覚えなくてはならないのがLongPtrである。
Declare文の64bit対応に関しては以前次のような記事を書いたが、LongPtrの置き換えの考え方に関しては説明を省略していた。
今回は64bit対応のためにPtrSafe/LongPtrが実装された経緯について、私なりの解釈について説明する。
あくまで個人的に「たぶんこうだったんだろう」と理解したことであり、公式に得た情報ではないので注意するように。
何故PtrSafeが生まれたのか
既に多くの読者は目にしたことがあると思うが、64bit版OfficeをインストールしたVBAではPtrSafeを書かないと無条件にコンパイルエラーとなる。
例えば、あるウィンドウをアクティブに切り替えるAPI:SetForegroundWindow
の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
詳しくは後ほど説明するが、「プログラマがちゃんと64bit対応のチェックをした」ということをコンパイラに一言で伝えるために生まれたのが「PtrSafe」である。
VBAコードの大多数はコンパイルエラーがあっても実際にプロシージャへ流入するまでエラーは出ないのだが、宣言セクションなどの全体に影響する部分のコンパイルエラーがある時は、一切のVBAの実行ができなくなるように出来ている。
64bit環境では、Declare文に一つでもPtrSafeが不足しているとプロジェクト全体が64bit非対応とみなし、そのAPIを使用するか否かに関わらず一切のプロシージャの実行を認めないというかなり厳しい制限を敷いている。
もし実行できてしまったら、VBAの不適切なメモリアクセスによってOffice自体がクラッシュすることになるので、そのくらい厳しい制限で良いのだ。
というわけで、「PtrSafeを書く」たったそれだけで「コンパイルエラーは出なくなる」 だが、本来すべきことはソコじゃない。
本当にやるべきことは、APIの仕様に合わせて、以下のような点を修正することである。
- 引数の型をLongPtrに変える
- Type(ユーザー定義型)メンバの型をLongからLongPtrに変える
- API関数に渡すデータ・変数の型をLongPtrに変える
- 戻り値の型をLongPtrに変える
問題となるのはLongだけとは限らなくて、StringやAnyなどからLongPtrに置き換えなければならないこともあるが、ややっこしいので事例紹介はLongだけにする。(修正の8割方はメモリ・ポインタ・ハンドルに関わるLongの部分なので、まずはここから抑えていくと良い)
一方でLongからLongPtrに変更してはならない引数もある。だからこそ仕様を確認しなければならない。
先の通り、PtrSafe
は「ちゃんとチェックしたよ!!!」と宣言するために書くべきなのだが、実態は「とりあえず書いとけ」状態になっている。よく分かっていないプログラマにとって、コンパイルが通らないことには始まらないので、小難しい部分の変更が後回しになるのは仕方がないのだが、それではMicrosoftがこのような安全措置を取った意味を成さないのでちゃんと調べなければ、後で絶対に後悔する。
Officeのクラッシュによって未保存のデータが失われ、リリース後のクラッシュによって休日出勤する羽目になることは断言できる。
LongLongとは一体何なのか
LongPtrの話をする前に、LongLongが何者なのか知らなければならない。
LongLongとは64bit OSでのみ使用可能なデータ型の一つである。
少し話が逸れるが、32bitOSの時代を過ごした方なら、メモリの上限に苦しめられた人が少なくないのではないだろうか?
まず、32bit OSではメモリを3GB強しか認識されなかった。2GBx2本を搭載して4GBにしても、実際に使えるのは3.3GBとかその程度で、残りの0.7GBは使用できなかった。
更に、アプリケーションは1つのプロセスで2GBまでしか使用できないという制限があり、メモリの食う画像処理では重い足かせとなっていた。(尤も、器が3,3GBしかないので微々たる差だが)
OSかアプリかの違いはあるものの、64bitに変化したことで得られるメリットは、このメモリ制限の撤廃だ。64bit化されたことで、今後数百年は使い切れないほど無限のメモリを使う事ができるようになった。
64bit OSになって、なぜ使えるメモリの容量が増えたのかと言えば、メモリアドレスの桁数が増えたからに他ならない。
32bit環境では2進数で32桁までのメモリアドレスしか格納できなかった。2の32乗=4,294,967,296=4GiBである。
その上、プロセスには2GBの壁があった。*1
プロセスとは、実行中のアプリケーションのことで、Excel/VBAのことでもある。
OSに32bit/64bitがあるように、アプリケーションにも32bit用と64bit用がある。
32bitOSでは32bitアプリしか実行できない。同じように、64bitOSでは本来64bitアプリしか実行できない。
しかし、それでは過去のアプリが全て動かなくなってしまうため、32bit向けアプリケーションは64bitあるメモリアドレスの半分だけで実行するという仕組みによって互換性を維持した。
このため、64bit OSで実行される32bitアプリケーションは、1プロセス2GBの壁を引き継いでいる。いや、搭載されているメモリが広大になったので、32bitOS時代よりも影響が大きいと言っても良い。
だからころ、高負荷処理を行うアプリが次々と64bitアプリ化されていき、ついにOfficeも64bit版が選択できるようになったという経緯がある。
メモリアドレスというのは、普通のVBAで直接扱うことはない。でも、WindowsAPIを使う場合は頻繁に必要となる。
従来の32bitアプリケーションでは、VBAのLong型の変数がメモリアドレスと同じ大きさだった。
ところが64bitアプリケーションでは、メモリアドレスの桁数が2倍に増えて、Long型に入りきらなくなってしまった。
こうして、64bit分のメモリアドレスが格納できるように、Long型の2倍の桁数を持つLongLong型が追加されたと考えられる。
何故LongPtrが生まれたのか
だが、実際にLongLongを見かけることはめったに無い。
WEBでAPIのサンプルをみていると、どこもかしこもLongPtrだと思う。
なぜなら、その方が都合が良いからである。
LongLongよりも、環境に応じてデータ型変化する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
で分岐しなければならない。
これは、あまりにも冗長すぎるのではないだろうか?
同じ関数のために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
従って、基本的にLongLongの代わりにLongPtrを使えば問題ないということを意味する。
今後どうしたら良いのか
LongPtrが原因で本質を理解しずらいが、すべきことは大きく分けて2つある。
本当は、やることは他にもまだある。
- 符号なし整数への対策
- VarPtrへの対策 ...
1. Declare文の修正
まずは宣言側の修正が必要だ。 * Declare文の引数の型をLongPtrに変える * Declare文の戻り値の型をLongPtrに変える * Declare文の引数でType(ユーザー定義型)を参照渡ししている場合、Typeのメンバの型も適時LongからLongPtrに変える
この修正に関しては、Win32API_ptrsafe.txt
からコピペするなり、原典資料を読んで仕様から置き換えれば比較的容易に対応できる。
2. APIを呼び出す側の引数や戻り値のデータ型の修正
「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
ここまでは、コンパイルエラーに頼りながら修正していけば、そこまで苦労せずに対応できる。
3. 符号なし整数への対策
難しいのはここから。
VBAには符号なし整数(Unsigned Int/Long)型がないので、厳密にはLongを使うと不味い場面がある。
コンパイル時点では検知不可能なので、VBA実行中に思ったとおりに動かなくて問題が判明する。
コード上で自力で修正を加えたり、あえてCurrency型を活用するという方法がある。
詳しい解説は、また別の機会にでも。
4. VarPtrへの対策
特に恐ろしいのはVarPtrで変数のポインタを渡す場面だ。
コンパイル時点では検知不可能なので、VBA実行中にExcelがクラッシュする。
LongLongのポインタを渡すべきところでLongのポインタを渡したら、API内部でLong以上のブロックのメモリに書き込むことになるためメモリが破壊されてしまう。
だからStrPtr
とかVarPtr
とかで渡しているところは全部チェックしないといけない。
これも、詳しい解説は、また別の機会にでも。
よくある失敗
とりあえず、PtrSafe付けとけば良いんじゃないの?
ダメです。
それは問題の先送りです。
32bit環境では全く問題ありませんが、64bit環境では「クラッシュ」という形で問題が現れます。
とりあえず、Longを全部LongPtrに変えとけば良いんじゃないの?
ダメです。
32bit環境では全く問題ありませんが、64bit環境では「クラッシュ」という形で問題が現れます。
LongをLongLongに入れることはできるが、LongLongをLongに入れるのは不可能です。
Long型引数を持つAPIにLongPtrで宣言した変数を渡すとコンパイルエラーが出ます。
また、一旦LongPtrに修正したものの、後日適切なコードに直すためLongに戻すという無駄な労力が発生します。
Typeのメンバの型も注意が必要で、APIが書き込む構造体の大きさは明確に決められているので、LongにすべきところをLongLongにしてしまったら大きさが仕様と変わってしまいます。
当然、そのような構造体を渡したら、意図しないメモリアドレスにデータを書き込むのでクラッシュします。
だから何でもかんでもLongPtrにすれば良いというものではありません。
32bit環境しかないけど、64bit対応できたつもりなのでPtrSafe付けてもいいですか?
ダメです。
32bit環境で64bit対応に挑戦するのは悪手です。
ノーテストで64bitに対応するのは不可能です。
32bit環境でLongPtrはLongとして動作する。つまり全部LongPtrに変えてもエラーが出るわけがないのです。
APIを使った開発をする人は最低でも64bit環境で開発・テストしてください。
2013以降のOfficeであればオンラインでインストーラを入手できるので、32bitはアンインストールして64bitに入れ替えましょう。
また、64bit環境でテストが通ったからと言って32bit環境で絶対に動くとは限らないため、面倒かつ財布に厳しい話ではありますが両方を保有してテストを通すことを強く推奨します。
幸い365のサブスク契約をしているなら5台のパソコンにインストールが可能なので、別のパソコンか仮想マシン上のWindowsにインストールしてテストしましょう。
参考資料
以前、こんな連続ツイートをした。
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 からコピーせよ。
しかし、書かれていない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文の置換ツールを開発した話。
以前、整数型の速度を比較した結果、64bitではLongLong(LongPtr)を使うのが一番高速という話。
まとめ
今日はLongPtrが何者か説明してみました。
私から言える事は一つ。
今後もAPIを使うなら、開発者のExcelは64bitにしておけ。
あとはじっくりとテストするしか無い。
そうしないと配布してから苦しむことになる。
私のようにね。
現場からは以上です。
何か御座いましたらコメント欄、またはTwitterからどうぞ♪
それではまた来週♪ ちゅんちゅん(・8・)
*1:「アレヤコレヤで4GBから更に削られて、3GBの壁や2GBの壁があった」というイメージで、私はなんとなく理解しているので間違っていたら申し訳ございません。