回调函数实现面向对象
什么是回调函数?
通常,Windows均要求我们将消息处理函数定义为一个全局函数,或者是一个类中的静态成员函数。并且该函数必须采用__stdcall的调用约定(Calling Convention)
在VC中函数前有callback,就认为是回调函数,回调函数是__stdcall调用约定,参数是从右边开始压入栈的,所以第一个参数就在栈的最上层
在使用VC开发的时候,回调函数,都必须是全局的,或者类的静态函数方式出现,这样子就会破坏了类的封装性,使得我们很难发挥C++的优势
比如:::SetWindowLong子类化窗体时用到的回调函数
LRESULT CALLBACK WindowProc(HWND hwnd,
UINT uMsg,
WPARAM wParam,
LPARAM lParam
);
子类化窗体的回调函数还好处理些,可以将句柄与类的对应关系保存起来。或者使用::SetProp类似这种函数在窗体中设置一样标志,保存this指针,然后用
::GetProp取出来,类似下面这样的代码
//设置回调
void SubWindow::Test()
{
::SetProp( hWnd, _T("AAA"), this);
::SetWindowLong( hWnd, GWL_WNDPROC, WindowProc);
}
LRESULT SubWindow::Window_Proc(.......)
{
}
LRESULT CALLBACK WindowProc(HWND hwnd,
UINT uMsg,
WPARAM wParam,
LPARAM lParam
)
{
SubWindow *pthis = ::GetProp( hwnd, _T("AAA"));
if ( NULL != pthis )
{
pthis->Window_Proc(....);//这样就回到类里面去了
}
}
那么,其它的回调函数,要如何调呢??
这个时候就要使用一种叫做Thunk的技术,在ATL中有微软写好的代码,实现原理是改变回调函数的第一个参数,使第一个参数为this指针
即为指针,那就是说明,回调函数至少需要一个参数,32位下,第一个参数,可以是4字节的任何一种,但在64位下,就必须是64位的
要想在32位也能用,64位也能用,第一个参数就必须是指针类型,如HWND HHOOK 编译成64位程序时自动就是64位
Thunk的原理,可以在网上找到,或去我的网站 www.panshsoft.com 搜这个关键字查找,在这里就不多说了
以前在 32位系统下,我一直是用自己写的Thunk的,后来,发现不兼容64位,所以改成使用 ATL的Thunk,这个支持很多种平台。
附上我写的Thunk,供大家学习
使用方法,如下
Class Test()
{
public:
void subclass()
{
m_thk.Alloc_Thunk(WindowProc, this);
::SetWindowLong( hWnd, GWL_WNDPROC, m_thk.GetThunkData());
}
public:
Thunk m_thk;
}
/////////////////////////////////////////////
#pragma once
class Thunk
{
public:
#pragma pack(push, 1)// 该结构必须以字节对齐
typedef struct
{
BYTE AsmCall;//CALL(0xE8) //第一步调用CALL
LONG OffsetAdder;//偏移址
LONG Proc; //回调函数指针
BYTE AsmPOP; //POP ECX
//开始保存参数
BYTE AsmMov[4]; //8B 44 24 04
BYTE AsmMovEax;
LONG AsmMoveEaxAddr;
//替换堆栈值
BYTE AsmMovChange[4];
LONG NewParameter;
//调用回调函数
BYTE Jmp;
BYTE ECX;
//----
LONG OldParameter;
DWORD dwData;//附加值
}THUNK_STRUCT, *LP_THUNK_STRUCT;
#pragma pack(pop)
Thunk():m_pThunkData(NULL)
{
}
~Thunk()
{
Free_Thunk();
}
/********************************************************************
**【函 数 名:】 Alloc_Thunk
**【参in 数:】 lCallBackFunPtr 回调函数的指针
**【参in 数:】 dwData用户附加的数据
**【返 回 值:】
**【作 者:】 磐实
**【日 期:】2011/11/26
**【修 改 人:】
**【日 期:】
**【版 本:】
**【详细说明:】使用回调函数时可以将回调函数导入到类中处理,实现OOP思想
**【详细说明:】如:SetWindowLong(m_hWnd, GWL_WNDPROC, &ts)
**【详细说明:】本函数只改更第一个参数为4字节的回调函数
********************************************************************/
inline LP_THUNK_STRUCT Alloc_Thunk(LONG lCallBackFunPtr, DWORD dwData)
{
if(NULL != m_pThunkData)
{
return m_pThunkData;
}
m_pThunkData = (LP_THUNK_STRUCT)::VirtualAlloc(NULL, sizeof(THUNK_STRUCT),
MEM_COMMIT, PAGE_EXECUTE_READWRITE);
if(m_pThunkData != NULL)
{
IniThunk(m_pThunkData, lCallBackFunPtr, dwData);
}
return m_pThunkData;
}
inline void Free_Thunk()
{
if(m_pThunkData != NULL)
{
::VirtualFree(m_pThunkData, 0, MEM_RELEASE);
m_pThunkData = NULL;
}
}
inline LP_THUNK_STRUCT GetThunkData()
{
return m_pThunkData;
}
private:
/********************************************************************
**【函 数 名:】 IniThunk
**【功能描述:】 初始化Thunk结构体
**【参io 数:】 需要初始化ts的结构体,必需为全局或类成员变量
**【参in 数:】 lCallBackFunPtr 回调函数的指针
**【参in 数:】 dwData用户附加的数据
**【返 回 值:】
**【作 者:】 磐实
**【日 期:】2011/11/26
**【修 改 人:】
**【日 期:】
**【版 本:】
**【详细说明:】使用回调函数时可以将回调函数导入到类中处理,实现OOP思想
**【详细说明:】如:SetWindowLong(m_hWnd, GWL_WNDPROC, &ts)
**【详细说明:】本函数只改更第一个参数为4字节的回调函数
********************************************************************/
void IniThunk(LP_THUNK_STRUCT pts, LONG lCallBackFunPtr, DWORD dwData)
{
pts->AsmCall = 0xE8;//call
//跳过Proc参数的字节数
pts->OffsetAdder = (DWORD)&(((LP_THUNK_STRUCT)0)->AsmPOP)-
(DWORD)&(((LP_THUNK_STRUCT)0)->Proc); //偏移量
//执行AsmCall 0xE8 后会把ts.Proc压入堆栈
//pop ecx,Proc已压栈,弹出Proc到ecx
pts->AsmPOP = 0x59;//pop ecx
//Proc已弹出,栈顶是返回地址,紧接着就是第一个参数了。
//[esp+0x4]就是第一个参数
//8B 44 24 04 保存第一个参数到eax寄存器
//mov eax,dword ptr [esp+4]
pts->AsmMov[0] = 0x8B;
pts->AsmMov[1] = 0x44;
pts->AsmMov[2] = 0x24;
pts->AsmMov[3] = 0x04;
//mov [t+1 (00416881)],eax
//将eax的值保存到变量中
pts->AsmMovEax = 0xA3;
//变量的地址
pts->AsmMoveEaxAddr = (LONG)&pts->OldParameter;
//改变原来函数的参数值
pts->AsmMovChange[0] = 0xC7; // mov
pts->AsmMovChange[1] = 0x44; // dword ptr
pts->AsmMovChange[2] = 0x24; // disp8[esp]
pts->AsmMovChange[3] = 0x04; // +4
pts->NewParameter = (LONG)pts;
//调用用户传进来的回调函数
//jmp ecx
pts->Jmp = 0xFF; // jmp [r/m]32
pts->ECX = 0x21;// [ecx]
//附加值
pts->dwData = dwData;
pts->Proc = lCallBackFunPtr;//回调函数指针
::FlushInstructionCache(::GetCurrentProcess(),
pts, sizeof(THUNK_STRUCT));
return ;
}
private:
LP_THUNK_STRUCT m_pThunkData;
};
/////////////////////////////////////////////////////////////////////////////////////////////
如果想让上面的代码在64位下使用,必须改变上面的机器码为64位的。
这里只介绍使用微软提供的ATL Thunk的使用方法.
Thunk更详细的说明,这里就不说了,只要了解是换第一个参数在栈中的值就行,将这个值变成this指针值.
ATL Thunk 代码原理也不讲解,因为涉及的知识面很多 大至有,函数调用约定,机器语言等。
ATL为我们提供了很好的Thunk代码,我们只要使用就可以,而且还支持各种各样的CPU平台
文件
atlstdthunk.h
atlthunk.cpp
想深入研究的,可以搜一下VC安装目录,看不懂的可以登录
bbs.panshsoft.com 发贴问
下面直接上例子,供大家参考
使用时必然包含
#include <atlstdthunk.h>
using namespace ATL;
调用CStdCallThunk类就可以实现
子类化时使用
.cpp
void CThunkWindowDlg::OnBnClickedOk()
{
// TODO: Add your control notification handler code here
//OnOK();
m_pThunkSubClass = new CStdCallThunk;
m_pThunkSubClass->Init( (DWORD_PTR)CThunkWindowDlg::pWindowProc, this );
m_oldSubClass = (WNDPROC)::SetWindowLongPtr( m_hWnd, GWLP_WNDPROC,
(LONG_PTR)m_pThunkSubClass->GetCodeAddress() );
MessageBox( _T("成功使用Thunk,请点击窗体,会弹出一个框,说明Thunk在运行") );
}
.h
public:
static LRESULT CALLBACK pWindowProc( HWND hwnd,
UINT uMsg,
WPARAM wParam,
LPARAM lParam
)
{
CThunkWindowDlg* pThis = (CThunkWindowDlg*)hwnd;
return pThis->Window_Proc( uMsg, wParam, lParam );
}
LRESULT Window_Proc( UINT uMsg,WPARAM wParam,
LPARAM lParam )
{
if ( uMsg == WM_LBUTTONDOWN )
{
MessageBox( _T("Thunk成功 子类化 调里类中的") );
}
if ( uMsg == WM_NCDESTROY )
{
::SetWindowLongPtr( m_hWnd, GWLP_WNDPROC,
(LONG_PTR)m_oldSubClass );
delete m_pThunkSubClass;
m_pThunkSubClass = NULL
return 1;
}
return ::CallWindowProc( m_oldSubClass, m_hWnd, uMsg, wParam, lParam );
}
private:
WNDPROC m_oldSubClass;
CStdCallThunk *m_pThunkSubClass;
总结: 使用Thunk缺点是,回调函数,要有参数,而且第一个参数,还必须是和程序编译后位数一样,32位的要是32位,
64位要是64位,这就是我为什么在上面提到第一个参数要是指针类型的原因,指针类型会随着编译后变化
别一个缺点是,第一个参数,必须在使用Thunk之前保存后的,要不然,在使用Thunk后会丢失!!!!!
像HWND 事先都是保存好的
使用我开发的那个Thunk可以在32位不管哪种情况都可以,因为有一个 LONG OldParameter; 保存着,回调函数的第一个值
所以是不会丢失第一个参数的,这样 ::SetWindowsHookEx也可以使用Thunk技术啦!!!!!!
但不支持64位
相关文章与源码下载地址 其中有支持32位与64位的thunk
www.panshy.com/article/Sort_Desktop/other/2014-04-09/2473.php
www.panshy.com/download/demo_code/fun_class_code/2014-04-09/231.php
www.panshy.com/download/demo_code/UI/2013-08-11/70.php