打开APP
userphoto
未登录

开通VIP,畅享免费电子书等14项超值服

开通VIP
(转)如何做类的回调函数 - Amadeus的日志 - 网易博客

如何做类的回调函数


 

前些日子用一个PIPE类封装了WINDOWS的录音放音设备,程序写得有点类似与操作系统的PV信号互锁机制,这里面需要将辅助录音现程采集到的数据存储到Buffer,然后做一个回掉函数做处理,以下是几种实现回掉的方法,程序最后使用了THUNK机制,再说明这种机制之前。先澄清一下回掉函数的概念。

所谓回调,就是客户程序C调用服务程序S中的某个函数A,然后S又在某个时候反过来调用C中的某个函数B,对于C来说,这个B便叫做回调函数。例如Win32下的窗口过程函数就是一个典型的回调函数。
一般说来,C不会自己调用B,C提供B的目的就是让S来调用它,而且是C不得不提供。由于S并不知道C提供的B叫甚名谁,所以S会约定B的接口规范(函数原型),然后由C提前通过S的一个函数R告诉S自己将要使用B函数,这个过程称为回调函数的注册,R称为注册函数。

1。传统的CALLBACK

传统SDK回调函数设计模式
Win32SDK是这方面的典型例子,这类SDK的函数接口都是基于C语言的,SDK或者提供专门的注册函数,用于注册回调函数的地址,或者是在调用某个方法时才传入回调函数的地址,回调函数的原型也由于注册函数中的函数指针定义而受到约束。 以Win32中的多媒体定时器函数为例,其原型为:
MMRESULT timeSetEvent(
UINT uDelay, // 定时器时间间隔,以毫秒为单位
UINT uResolution,
LPTIMECALLBACK lpTimeProc, // 回调函数地址
DWORD dwUser, // 用户设定的数据
UINT fuEvent
);
typedef void (CALLBACK TIMECALLBACK)(UINT uTimerID, UINT uMsg, DWORD dwUser, DWORD dw1, DWORD dw2);
typedef TIMECALLBACK FAR *LPTIMECALLBACK;
因此,用户定义的回调函数必须具有上面指定的函数原型,下面是回调函数的具体使用方法:
#include "stdio.h"
#include "windows.h"

#include "mmsystem.h" // 多媒体定时器需要包含此文件
#pragma comment(lib, "winmm.lib") // 多媒体定时器需要导入此库

void CALLBACK timer_proc(UINT uTimerID, UINT uMsg, DWORD dwUser, DWORD dw1, DWORD dw2) // 定义回调函数
{
printf("time out.\n");
}

int main()
{
UINT nId = timeSetEvent( 1000, 0, timer_proc, 0, TIME_CALLBACK_FUNCTION|TIME_PERIODIC); // 注册回调函数
getchar();
timeKillEvent( nId );
return 0;
}

2。简单的实现类成员函数做CALLBACK的方法
C++出现后,CALLBACK函数提出了自身的局限性,因为CALLBACK函数是为C语言的函数设计的,而C++本身的类的this指针导致了回调C++的类成员函数不能得到其有效地址,解决这个局限其实也简单,基本方法有两种
1. 不使用成员函数,直接使用普通C函数,为了实现在C函数中可以访问类的成员变量,可以使用友元操作符(friend),在C++中将该C函数说明为类的友元即可。这种处理机制与普通的C编程中使用回调函数一样。
2.使用静态成员函数,静态成员函数不使用this指针作为隐含参数,这样就可以作为回调函数了。静态成员函数具有两大特点:其一,可以在没有类实例的情况下使用;其二,只能访问静态成员变量和静态成员函数,不能访问非静态成员变量和非静态成员函数。由于在C++中使用类成员函数作为回调函数的目的就是为了访问所有的成员变量和成员函数,如果作不到这一点将不具有实际意义。解决的办法也很简单,就是使用一个静态类指针作为类成员,通过在类创建时初始化该静态指针,如pThis=this,然后在回调函数中通过该静态指针就可以访问所有成员变量和成员函数了。这种处理办法适用于只有一个类实例的情况,因为多个类实例将共享静态类成员和静态成员函数,这就导致静态指针指向最后创建的类实例。为了避免这种情况,可以使用回调函数的一个参数来传递this指针,从而实现数据成员共享。这种方法稍稍麻烦,这里就不再赘述。

3 将类成员函数写成虚函数,直接访问虚函数表,得到函数地址,再传递给调用函数 (wang2012629@126.com 的 方法),简单介绍如下
比如说DialogBox(...);
例子如下:
// TestCallbackDlg.h : header file
class CTestCallbackDlg : public CDialog
{
// Construction
public:
virtual LRESULT DlgProc (HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam);//放在最前面.

CTestCallbackDlg(CWnd* pParent = NULL); // standard constructor// TestCallbackDlg.cpp : implementation file
//

LRESULT CTestCallbackDlg::DlgProc (HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
{
switch (message)
{
case WM_COMMAND:
{
TRACE("DlgProc %d\r\n",hWnd);
if( wParam ==IDOK)
{
//AfxMessageBox("gfhdfhfgh");/*前后都不响应,MessageBox不消失?*/
::MessageBox(hWnd,"HAHAHA","dfgdfgh",MB_OK);
::EndDialog(hWnd,IDOK);
//AfxMessageBox("gfhdfhfgh");
}
break;
}
case WM_CLOSE:
{
AfxMessageBox("gfhdfhfgh");
::EndDialog(hWnd,IDOK);
break;
}
}
return ::DefWindowProc(hWnd, message, wParam, lParam);
}
//从虚函数表中获取函数地址.
void CTestCallbackDlg::OnOK()
{
// TODO: Add extra validation here
unsigned long pThis = (unsigned long) this;
DLGPROC TempFuncAddr = NULL;
_asm
{
mov eax,pThis;
mov edx,dword ptr [eax];
mov eax, dword ptr [edx+0D8h];
mov TempFuncAddr,eax;
}

TRACE("CTestCallbackDlg %d\r\n",this->m_hWnd);
//TempFuncAddr中就是函数LRESULT CTestCallbackDlg::DlgProc(...)的地址

DialogBox(AfxGetApp()->m_hInstance, MAKEINTRESOURCE(IDD_ABOUTBOX), this->m_hWnd, (DLGPROC)TempFuncAddr);
}

4。利用抽象类CWndProc
//wndpro.h
#ifndef __WNDPROC_H__
#define __WNDPROC_H__


class CWndProc
{
protected:
//保护的构造函数,必须由派生类来构造。
CWndProc();
virtual ~CWndProc();

protected:
//窗口回调过程,基类作为纯虚函数没有实现代码。
virtual LRESULT CALLBACK WndProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam) = 0;

private:
//Hook代码块。
char m_hook[40];

protected:
//m_pfnWndProc指针指向Hook代码块的始地址。
//注册窗口类(WNDCLASSEX),或者子类化控件窗口,或者DialogBox显示对话框
//等需要窗口回调过程参数时,使用m_pfnWndProc作为参数。
WNDPROC m_pfnWndProc;
};


#endif //__WNDPROC_H__

//end of file
二、实现代码文件 //wndproc.cpp

#include "stdafx.h"
#include "wndproc.h"


/*
全局的Hook代码,其C的伪代码为:
LRSULT CALLBACK WndProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
{
return (CWndProc派生类的this指针)->WndProc(hwnd, uMsg, wParam, lParam);
}
代码的功能就是直接转调用CWndProc派生类的WndProc。
*/

static unsigned char g_hook[] =
{
0x8B, 0x44, 0x24, 0x10, // mov eax,dword ptr [esp+10h] ; eax <- lParam
0x8B, 0x4C, 0x24, 0x0C, // mov ecx,dword ptr [esp+0Ch] ; ecx <- wParam
0x8B, 0x54, 0x24, 0x08, // mov edx,dword ptr [esp+8] ; edx <- uMsg
0x50, // push eax ; lParam 参数入栈
0x8B, 0x44, 0x24, 0x08, // mov eax,dword ptr [esp+8] ; eax <- hwnd
0x51, // push ecx ; wParam 参数入栈
0xB9, 0x00, 0x00, 0x00, 0x00, // mov ecx,0 ; ecx <- this指针,这里暂时用this(NULL),
// ; 在类构造函数初始化时修改为实际类的this指针值
0x52, // push edx ; uMsg 参数入栈
0x50, // push eax ; hwnd 参数入栈
0x51, // push ecx ; this 参数入栈
0xE8, 0x00, 0x00, 0x00, 0x00, // call WndProc ; 调用派生类的WndProc,这暂时用0,
// ; 在类构造函数初始化时修改为实际类虚拟表WndProc指针偏移值
0xC2, 0x10, 0x00 // ret 10h ; return
};


CWndProc::CWndProc()
{
char *p;
LRESULT (CALLBACK CWndProc::*pfn)(HWND, UINT, WPARAM, LPARAM);

CopyMemory(m_hook, g_hook, sizeof(g_hook)); //把全局的Hook代码块,拷贝到类对象的Hook代码块
p = m_hook + 19; //p指针指向 mov ecx, 0 处,以便修改this(NULL)指针为实际类对象的this指针
*((unsigned int *)p) = (unsigned int)this; //修改p所指向的位置为mov ecx, (指向实际的类对象的this指针)

pfn = WndProc; //pfn指向类虚拟表中WndProc函数的指针;
p = m_hook + 27; //p指针指向 call WndProc处,以便修改WndProc虚表指针相对偏移值
//由于vc6.0无法修改pfn及强制其类型,所以下面使用几句汇编
__asm
{
mov eax, pfn ; eax <- pfn
sub eax, 4 ; eax <- eax-4
mov edi, p ; edi <- p指针
sub eax, edi ; eax <- eax-edi 计算WndProc虚表指针与当前 EIP+5 相对偏移值
mov [edi], eax ; eax <- [edi] 修改p所指向的位置为 call (WndProc虚表指针与当前 EIP+5 相对偏移值)
}
m_pfnWndProc = (WNDPROC)&m_hook[0]; //把Hook代码块始址赋给m_pfnWndProc
}

CWndProc::~CWndProc()
{
}

五。导入类指针地址作为成员函数参数typedef void (*HELP_CALLBACK)(const char*, unsigned long); // 回调函数指针
void help_me( const char* question, HELP_CALLBACK callback, unsigned long user_value ); // 接口声明

#endif//__SDK_H__
#include "sdk.h"
#include "stdio.h"
#include "windows.h"

HELP_CALLBACK g_callback;
unsigned long g_user_value;

void do_it()
{
printf("thinking...\n");
Sleep( 3000 );
printf("think out.\n");
printf("call him.\n");
g_callback( "2.", g_user_value ); // 将用户设定的数据传入
}

void help_me( const char* question, HELP_CALLBACK callback, unsigned long user_value )
{
g_callback = callback;
g_user_value = user_value; // 保存用户设定的数据
printf("help_me: %s\n", question);
do_it();
}

/// app.cpp (应用程序源文件)
#include "sdk.h"
#include "stdio.h"
#include "assert.h"

class App
{
public:
App( const char* name ) : m_name(name)
{
}
void ask( const char* question )
{
help_me( question, got_answer, (unsigned long)this ); // 将this指针传入
}
static void got_answer( const char* msg, unsigned long user_value )
{
App* pthis = (App*)user_value; // 转换成this指针,以访问非静态数据成员
assert( pthis );
printf("%s got_answer: %s\n", pthis->m_name, msg);
}
protected:
const char* m_name;
};

int main()
{
App app("ABC");
app.ask( "1+1=?");
return 0;
}

六:最酷的方法,THUNK机制(感谢北理论坛的detrox ,这个方法太棒了)
在VC++中,所有类的成员函数在定义的时候都被隐式(implicit)定义为__thiscall参数传递方式。在MSDN 中对__thiscall做了如下定义:
引用:
The__thiscall calling convention is used on member functions and is thedefault calling convention used by C++ member functions that do not usevariable arguments. Under __thiscall, the callee cleans the stack,which is impossible for vararg functions. Arguments are pushed on thestack from right to left, with the this pointer being passed viaregister ECX, and not on the stack, on the x86 architecture.

其中心思想是,__thiscall 修饰的函数参数从右至左依次压入堆栈,被调用者负责平衡堆栈。之后是与C语言所有参数传递方式均不相同的一点:成员函数所在类的this指针被存入ecx寄存器(这个特性只针对Intel x86架构)。

对比之后,我们发现类成员函数不能作为回调函数的主要原因在于类成员函数使用__thiscal参数传递方式,因此需要调用者(caller)通过ecx寄存器提供类对象的指针。而回调函数使用__stdcall参数传递方式,不具备这个特点。

如何让类成员函数成为回调函数
------------------------------------------------------------------
根据第一节对回调函数与类成员函数各自特点的分析。不难发现,只要能想办法在类成员函数被调用之前设置好ecx寄存器,就能在__stdcall调用的基础上模拟出一个完好的__thiscall调用。

如何提前设置ecx寄存器呢?我们知道函数调用实际是通过汇编指令(oprand)’call函数地址’完成的。因此我们可以提供一个中间函数。当回调发生时,先调用中间函数,再在中间函数执行过程中设置ecx寄存器,当ecx设置好后jmp到类成员函数去(注意:这里是jmp不是call)。当执行到类的成员函数时,函数上下文(functioncontext)就和__thiscall所产生的完全一样了。

如何制作这个中间函数呢?普通的函数是不行的。主要因为在vc++ debug版本的代码中要使用ecx寄存器做堆栈溢出检测(stack overflow detect),即使是空函数都是如此。其次由于存在栈框(stack frame)效率也不高。
这时就需要使用thunk来达到我们的目的。所谓thunk就是程序自己生成并执行的一小段汇编代码。下面通过代码来理解thunk。
thunk实现:
引用:
以下代码在Windows XP SP2, Visual Studio 2005 Team Suite 下调试成功
所用处理器 Intel Pentium M 750, Dothan Core
cpp 代码 [复制到剪贴板]#include "windows.h"
#include "stdio.h"
#include "stdlib.h"
#include "assert.h"
#include "stdafx.h"
//////////////////////////////////////////////////////////////////////////
// 回调函数类型定义
typedef int (CALLBACK *pfaCallBack)(int, long, char);
/////////////////////////////////////////////////////////////////////////
// thunk 结构定义
// 由于thunk 要被当作代码来执行,因此thunk 结构必须是字节对齐的,这里使用
// VC++ 的修饰符号#pragma pack(push, 1) 来定义一个字节对齐的结构体
// 之后通过#pragma(pop) 恢复默认对齐模式
#pragma pack(push, 1)
struct Thunk
{
BYTE op_movecx;
DWORD_PTR val_ecx;
BYTE op_call;
DWORD_PTR val_address;
};
#pragma pack(pop)
//////////////////////////////////////////////////////////////////////////
// 一个类的定义,就这样平静的开始了
class Dummy {
// 一个成员变量
private:
int m_id ;
// 定义一个thunk
private:
Thunk m_thunk;
// 定义构造函数,在构造函数中设置m_id值
public:
Dummy(int id):m_id(id)
{ }
/////////////////////////////////////////////////////////////////////////
// 定义一个回调函数,另外他还是个类的成员函数呢
public:
int memberCallback(int intVal, long longVal, char charVal)
{
// 做自己想做的事情
printf("\nI am a member function of class Dummy"
"(Dummy::memberCallback),ID = %d."
"\nI got the value 0x%08x 0x%08x \'%c\'"
, m_id, intVal, longVal, charVal);
return m_id;
}

//////////////////////////////////////////////////////////////////////////
// 初始化thunk 的数据,这里是关键
public:
void InitThunk()
{
// 0xB9是‘mov ecx, 数值’的机器码,xB9之后的个字节(32位)指定了要

// 给ecx的数值.
m_thunk.op_movecx = 0xB9;
// 填写要给ecx的数值为this(类对象的指针)
m_thunk.val_ecx = (DWORD_PTR)this;
// 0xE9是‘jmp 相对地址’的机器码。相对地址由xE9之后的个字节(32位)
// 给出。
m_thunk.op_call = 0xE9;
// 获得Dummy::memberCallback的具体地址。关于成员函数与类对象的关系
// 请参阅Stan Lippman 的<<Inside C++ Object Model>>
// 用汇编获得地址省去了用C++带来的难看的语法
DWORD_PTR off = 0;
_asm
{
mov eax, Dummy::memberCallback
mov DWORD PTR [off], eax
}
// jmp后面是相对地址,因此要求出这个地址
// 相对地址=成员函数地址-跳转点下一指令地址
// 正负号不要紧,jmp自己能根据正负判断如何跳。
m_thunk.val_address =
off - ( (DWORD_PTR)(&m_thunk.val_address) + sizeof(DWORD_PTR) );
}
//////////////////////////////////////////////////////////////////////////

// 返回thunk的地址给要回调他的函数。
// 那个函数还以为thunk是一个函数地址呢。根本不知道thunk是我们自己构造的

// 数据

public:

pfaCallBack GetStaticEntry()
{ return (pfaCallBack)&m_thunk;

}
};

//////////////////////////////////////////////////////////////////////////

// 一个调用回调函数的函数

void Trigger(pfaCallBack callback)

{

assert(callback);

int intVal = 0x1234;

int longVal = 0x5678ABCD;

int charVal = 'D';

// 函数内部

int r;

// 开始回调

r = callback(intVal, longVal, charVal);

printf("\n Return value = %d\n", r);

}



//////////////////////////////////////////////////////////////////////////

// 传说中的主函数。VC++工程里生成的就叫_tmain不叫main。

int _tmain(int argc, _TCHAR* argv[])

{

//生成一个对象

Dummy *dummy1 = new Dummy(9);

//初始化thunk

dummy1->InitThunk();

//取得thunk地址

pfaCallBack pCallback1 = dummy1->GetStaticEntry();

//给需要回调函数的函数传递thunk

Trigger(pCallback1);

// 按任意键继续...

system("pause");

return 0;

}

cpp 代码
pragma once

#include <Windows.h>

#include <cassert>

// BYTE Alignment



#pragma pack(push, 1)

struct ThunkData

{

struct {

BYTE opcode;

DWORD_PTR oprand;

} movecx;



struct {

BYTE opcode;

DWORD_PTR oprand;

} jmp;

};



#pragma pack(pop)



template <typename PointerType, typename CallbackType=void *>

class CThunk

{

public:

CThunk();

public:

~CThunk(void);

public:

void Attach(DWORD_PTR owner, PointerType PointerToMemberFunction);

public:

void * GetThunk() { assert(bAttached == true);return &m_thunkData;};

public:

operator CallbackType() {return (CallbackType)(&m_thunkData);};

private:

PointerType m_fPointer;

private:

ThunkData m_thunkData;

private:

bool bAttached;

};



template<typename PointerType, typename CallbackType>

CThunk<PointerType, typename CallbackType>::CThunk(): bAttached(false)

{

// make sure that this is stored in ecx

#ifdef _DEBUG

DWORD_PTR pthis;

_asm { mov dword ptr [pthis],ecx };

assert(pthis == (DWORD_PTR)this);

#endif

// mov ecx, this

m_thunkData.movecx.opcode = 0xB9;



m_thunkData.jmp.opcode = 0xE9;

}



template<typename PointerType, typename CallbackType>

CThunk<PointerType, typename CallbackType>::~CThunk(void)

{



}



template<typename PointerType, typename CallbackType>

void CThunk<PointerType, typename CallbackType>::Attach(DWORD_PTR owner, PointerType PointerToMemberFunction)

{

assert(owner);

assert(PointerToMemberFunction);



m_fPointer = PointerToMemberFunction;

DWORD_PTR FunctionOffset;

_asm

{

mov eax, PointerToMemberFunction

mov FunctionOffset, eax

}

m_thunkData.jmp.oprand = FunctionOffset - ( (DWORD_PTR)&m_thunkData.jmp.oprand + sizeof(DWORD_PTR) );

m_thunkData.movecx.oprand = owner;

bAttached = true;

}

使用方法

cpp 代码 [
class Dummy {

private:

int m_id ;

//////////////////////////////////////////////////////////////////////////

// Dummy Thunk

public:

CThunk<int (__thiscall Dummy::*)(int, long, char), pfaCallBack> DummyThunk;

CThunk<int (__thiscall Dummy::*)(int, long, char), pfaCallBack> DummyThunk2;



//////////////////////////////////////////////////////////////////////////

// Default constructor

public:

Dummy():m_id(0)

{

DummyThunk.Attach((DWORD_PTR)this, &Dummy::memberCallback);

DummyThunk2.Attach((DWORD_PTR)this, &Dummy::memberCallback2);

}

....

...
Trigger(dummy1->DummyThunk);
Trigger((pfaCallBack)dummy2->DummyThunk.GetThunk());
Trigger(dummy3->DummyThunk2);
...



cpp 代码
#define THUNK(NAME,FUNCTION) private: ThunkData m_##NAME##THUNK;\

public:\

void * NAME##Thunk() {\

DWORD_PTR dwptr;\

m_##NAME##THUNK.oprMOV = (DWORD_PTR)this;\

__asm {mov eax, FUNCTION}\

__asm {mov dwptr, eax}\

m_##NAME##THUNK.oprJMP = dwptr - (((DWORD_PTR)&m_##NAME##THUNK.oprJMP) + sizeof(DWORD_PTR));\

return &m_##NAME##THUNK;\

};\

使用方法

class Dummy {

THUNK(memberCallback, Dummy::memberCallback);

THUNK(AnotherCallback, Dummy::AnotherCallback);

private:

int m_id ;

....



cpp 代码
Trigger((pfaCallBack)dummy1->memberCallbackThunk());

Trigger((pfaCallBack)dummy2->AnotherCallbackThunk());

 

转自 黄申的博客

http://blog.sina.com.cn/qqhuangshen

本站仅提供存储服务,所有内容均由用户发布,如发现有害或侵权内容,请点击举报
打开APP,阅读全文并永久保存 查看更多类似文章
猜你喜欢
类似文章
【热】打开小程序,算一算2024你的财运
VC2008多重继承下的Virtual Functions:Adjustor Thunk技术
VC知识库文章
将成员函数作为回调函数
PE教程6: Import Table(引入表)
如何写一个简单的病毒程序
一个简单的C++程序反汇编解析
更多类似文章 >>
生活服务
热点新闻
分享 收藏 导长图 关注 下载文章
绑定账号成功
后续可登录账号畅享VIP特权!
如果VIP功能使用有故障,
可点击这里联系客服!

联系客服