後述のサンプル2では、CallFuncPtrという関数を定義している。この関数は、第一引数に呼び出したい関数のアドレス、第二引数以降にその関数へ渡したい引数を指定する。呼び出す関数に渡す引数には、文字列または32bit値を指定可能で、呼び出した関数内での引数の操作は反映されない。CallFuncPtrの戻り値は、呼び出した関数の戻り値と同値である。
例として、CallFuncPtrを使用してWin32のMessageBox関数によるメッセージボックスの表示を行う方法を示す。
Private Sub Command1_Click() Dim hDll As Long Dim pMessageBox As Long hDll = LoadLibrary("user32.dll") pMessageBox = GetProcAddress(hDll, "MessageBoxA") CallFuncPtr pMessageBox, Me.hWnd, "hello world", "test message", MB_OK FreeLibrary hDll End Sub
この例では、pMessageBoxという変数に、MessageBox関数のアドレスが格納され、CallFuncPtrによってそのアドレスが指す関数を実行している。
CallFuncPtr内ではどのような処理を行っているのだろうか? 実は、CallFuncPtrは、与えられた関数ポインタを実行するためのバイナリの実行コード(いわゆる「マシン語」コード)をメモリ上に生成し、これを実行することによって処理を実現している。具体的には、CallFuncPtr内部のpCodeDataという変数が指すメモリ領域に、以下のような実行コードを作成する。
55 push ebp ;引数をスタックへ退避 68 ** ** ** ** push n番目の引数 68 ** ** ** ** push n-1番目のの引数 … 68 ** ** ** ** push 1番目の引数 ;ポインタが指す関数を実行 B8 ** ** ** ** mov eax, 関数ポインタ FF D0 call eax ;戻り値を退避 BA ** ** ** ** mov edx, 返却格納場所 89 02 mov dword ptr [edx], eax ;終了 5D pop ebp 33 C0 xor eax,eax C2 08 00 ret 8
次に、このバイナリの実行コードをどのようにVBから実行させるのだろうか? これは、コールバック関数を使用するWin32 API関数を呼び出すことによって実現できる。これには色々なAPI関数が候補として考えられるが、CallFuncPtrでは、EnumObjectsを使用している。上記実行コードは、二つの引数を持ち、0を返すPASCAL呼び出し形式の関数の形になっている。従って、EnumObjectsに上記実行コードが格納されているアドレスを渡すことにより、生成した実行コードを実行することができる。
以上で示した方法は、バイナリの実行コードを生成し、これをAPI関数によって間接的にコールバック関数として呼び出す方法であった。この方法を応用すれば色々なことができるが、実際のプログラミングでは素直にVCや市販のOCXなどを当たったほうが良いだろう。
' ---------------- Form1.frmの内容 ---------------- Public Function CallFuncPtr(FuncPtr As Long, ParamArray Params() As Variant) As Long Const MAX_CODESIZE As Long = 65536 Dim I As Long Dim pCodeData As Long Dim pParamData() As Long Dim PC As Long Dim Operand As Long Dim RetValue As Long Dim StrValue As String Dim LongValue As Long '初期化 ReDim pParamData(UBound(Params)) As Long pCodeData = GlobalAlloc(GMEM_FIXED, MAX_CODESIZE) PC = pCodeData 'プロローグコード生成 AddByte PC, &H55 '渡されたパラメータの設定コード生成 For I = UBound(Params) To 0 Step -1 If VarType(Params(I)) = vbString Then pParamData(I) = GlobalAlloc(GMEM_FIXED, LenB(Params(I))) StrValue = Params(I) MoveMemory ByVal pParamData(I), ByVal StrValue, LenB(StrValue) Operand = pParamData(I) Else Operand = Params(I) End If 'Params(I)のパラメータ設定コード生成 AddByte PC, &H68 AddLong PC, Operand Next '呼び出しコード生成 AddByte PC, &HB8 AddLong PC, FuncPtr AddInt PC, &HD0FF 'エピローグコード生成 AddByte PC, &HBA AddLong PC, VarPtr(RetValue) AddInt PC, &H289 AddByte PC, &H5D AddInt PC, &HC033 AddByte PC, &HC2 AddInt PC, &H8 '呼び出し EnumObjects Me.hDC, OBJ_PEN, pCodeData, 0 '後処理 GlobalFree pCodeData For I = 0 To UBound(Params) If pParamData(I) <> 0 Then GlobalFree pParamData(I) Next CallFuncPtr = RetValue End Function '---------------- Module1.basの内容 ---------------- Declare Function GlobalAlloc Lib "kernel32" (ByVal Flags As Long, _ ByVal Size As Long) As Long Declare Function GlobalFree Lib "kernel32" (ByVal Mem As Long) As Long Declare Function EnumObjects Lib "gdi32" ( _ ByVal hDC As Long, _ ByVal ObjectType As Long, _ ByVal pEnumProc As Long, _ ByVal lParam As Long) As Long Declare Function MoveMemory Lib "kernel32" Alias "RtlMoveMemory" ( _ ByRef Dest As Any, ByRef Src As Any, ByVal Size As Long) As Long Public Const GMEM_FIXED As Long = 0 Public Const OBJ_PEN As Long = 1 Public Sub AddByte(ByRef PC As Long, ByVal ByteValue As Byte) MoveMemory ByVal PC, ByteValue, 1 PC = PC + 1 End Sub Public Sub AddInt(ByRef PC As Long, ByVal IntValue As Integer) MoveMemory ByVal PC, IntValue, 2 PC = PC + 2 End Sub Public Sub AddLong(ByRef PC As Long, ByVal LongValue As Long) MoveMemory ByVal PC, LongValue, 4 PC = PC + 4 End Sub