前書き
-
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)がセットされていることが確認できました。
後書き
あまり使わない機能ですので、不正確な部分や、説明が不足している箇所があるかもしれません。
添削や過不足あれば、お気軽にご指摘ください。













