前書き
-
VB6のソースをVB.NETに移行した場合に苦労するのがAPI呼び出しエラーの修正です。困ったときの助けになればと思います。
-
Visual Studioのネイティブデバッグ機能を使い、Windows API側に引き渡されている引数の値をAPI側から確認する手順を記載します。
- .NET側からはポインタや、アドレスの参照先がわからないのでネイティブデバッグします。
-
Declareの定義を修正、もしくは引数を修正後、本当に想定した値がセットされているのか確認することができます。
-
Visual Studioの初期設定では、ネイティブコードデバッグが有効になっていないため、あちこちの設定を変更します。
-
動作確認用のソースはこちらで公開しています。
環境
-
Visual Studio 2019 (Visual Basic)
VS2015以降であれば、ほぼ同じだと思います。
サンプルソース
-
CreateDCの呼び出しでエラーになります(バグっています。VB6のソースをコンバートしたイメージで書いています。)
-
プリンターの描画領域サイズをGetDeviceCaps()で取得するサンプルコードです。
-
DeleteDC()の3番目、4番目はnullを指定します(nullがないので0で代用)。本来はDEVMODE構造体を渡すのですが、nullでも動作上は問題ないです。
-
Module DebugPInvoke
' API定義
Declare Function CreateDC Lib "gdi32" Alias "CreateDCA" _
(ByVal lpDriverName As String, ByVal lpDeviceName As String,
ByVal lpOutput As String, ByVal lpInitData As Object) As Integer
Declare Function GetDeviceCaps Lib "gdi32" Alias "GetDeviceCaps" _
(ByVal hdc As Integer, ByVal nIndex As Integer) As Integer
Declare Function DeleteDC Lib "gdi32" Alias "DeleteDC" (ByVal hdc As Integer) As Integer
Sub Main()
Const PRINTER_NAME = "Microsoft Print to PDF"
' DeviceContext取得
Dim hDC = CreateDC("WINSPOOL", PRINTER_NAME, 0, 0)
' プリンターの描画サイズを取得
Dim width_pix = GetDeviceCaps(hDC, 8) ' 8:HORZRES, ピクセル単位の画面の幅
Dim height_pix = GetDeviceCaps(hDC, 10) '10:VERTRES, ピクセル単位の画面の高さ
Debug.WriteLine(PRINTER_NAME & " : " & width_pix & " × " & height_pix)
DeleteDC(hDC)
End Sub
End Module
デバッグのための準備
-
ネイティブデバッグの有効化
プロジェクトのプロパティー
⇒デバッグ
⇒ネイティブコードデバッグを有効にする
にチェック -
アドレスレベルのデバッグを有効にする
メニューバーの
ツール
⇒オプション
⇒デバッグ
⇒全般
⇒アドレスレベルのデバッグを有効にする
にチェック -
マイコードのみを有効にするのチェックを外します(ネイティブコードにステップインできるようにするため)
メニューバーの
ツール
⇒オプション
⇒デバッグ
⇒全般
⇒マイコードのみを有効にする
のチェックを外す -
シンボルサーバーからpdb読み込み有効化(スタックトレースにネイティブコードの関数名を表示するため)
メニューバーの
ツール
⇒オプション
⇒デバッグ
⇒シンボル
⇒Microsoftシンボルサーバー
にチェック※デバッグ開始時に多少時間がかかるようになります。
これで完了です。
ネイティブコード側からAPIの引数を確認する
ここではネイティブコードにステップインするため、デバッグを開始してから設定をを行います。
-
CreateDC()
行にブレークポイントセット(F9
) -
F5
でデバッグを開始して、ブレークポイント(CreateDC
)で止まるのを待ちます(シンボル情報をダウンロードするため少し時間がかかる) -
呼び出し履歴
を表示メニューバーの
デバッグ
⇒ウィンドウ
⇒呼び出し履歴
マイコードのみを有効にする
のチェックを外したため、ネイティブコードの関数名も表示されています。 -
ソースコードを右クリックして
逆アセンブリへ移動
をクリックソースコードが
逆アセンブル
された状態で表示されます
F5を押してエラー箇所まで実行する
-
例外が発生した箇所
gdi32full.dll!_GdiConvertToDevmodeW@4
で止まり、表示される例外ダイアログもネイティブコードの例外エラー(0xC0000005 メモリアクセス違反)に変わります。
CrateDC()
側から引数を確認する (スタックのベースポインタ(EBP)から探します)
-
呼び出し履歴
からgdi32.dll!_CreateDCA@16()
を選択します -
メモリ
ウィンドウを表示します(スタックとポインタの参照先を表示するため2つ表示します)メニューバーの
デバッグ
⇒ウィンドウ
⇒メモリ1
とメモリ2
をクリック※表示されない場合は
Ctrl + Alt + M, 1
(CtrlとAltとMを同時押しした後、1を押す) -
メモリウィンドウを右クリックして
4バイトの変数
と16進数で表示
を選択します32bitのプロセスで動いているため、4バイト単位に区切って表示をします。
-
メモリ1のアドレスに
ebp
と入力します。(ベースポインタ⇒スタック上に確保されているデータの格納領域の基点)ebp
はCPUのレジスタの名前です。詳細は下記のページなどを参照してくださいい。
-
左上から順に、ひとつ前のフレームのEBP、リターンアドレス(関数の戻り先アドレス)、引数1、引数2、引数3、引数4です。
アドレス 説明 008ff234 ひとつ前のフレームのEBP 6cdbf36d リターンアドレス 00d6c874 引数1 00d90864 引数2 00d8bcac 引数3 00000003 引数4 -
引数1「0x00D6C874」をメモリ2で見ると、確かに
WINSPOOL
が入っていることがわかります同様に引数2には
Microsoft Print to PDF
が入っています。 -
引数3と引数4を見ると、ソース上では
0(null)
を渡したつもりですが、なぜか0ではない値(00d8bcac,00000003)がセットされてます。
Dim hDC = CreateDC("WINSPOOL", PRINTER_NAME, 0, 0)
-
引数3の値のアドレスを見ると文字列で'0'が入っています。⇒引数の型をStringで宣言しているため、数値の0が文字に変換され、そのポインタがCrateDC()に渡されていることがわかります。
-
引数4の
00000003
は何かわかりませんが、少なくとも期待していた0
ではない値がセットされていることがわかります。VB側で渡した値と異なる原因はDeclareの宣言がまちがっているためです。修正してもう一度試してみます
APIの定義を修正して、もう一度引数を確認してみます
- 3番目の引数と、4番目の引数を
IntPtr
に変更します
Declare Function CreateDC Lib "gdi32" Alias "CreateDCA" _
(ByVal lpDriverName As String, _
ByVal lpDeviceName As String, _
ByVal lpOutput As IntPtr, _
ByVal lpInitData As IntPtr) As Integer
もう一度デバッグで確認すると、3番目の引数と4番目の引数に0(null)
がセットされていることが確認できました。
後書き
あまり使わない機能ですので、不正確な部分や、説明が不足している箇所があるかもしれません。
添削や過不足あれば、お気軽にご指摘ください。