回调函数是一个很有用,也很重要的概念。当发生某种事件时,系统或其他函数将会自动调用你定义的一段函数。回调函数在windows编程使用的场合很多,比如Hook回调函数:MouseProc,GetMsgProc以及EnumWindows,DrawState的回调函数等等,还有很多系统级的回调过程。本文不准备介绍这些函数和过程,而是谈谈实现自己的回调函数的一些经验。 之所以产生使用回调函数这个想法,是因为现在使用VC和Delphi混合编程,用VC写的一个DLL程序进行一些时间比较长的异步工作,工作完成之后,需要通知使用DLL的应用程序:某些事件已经完成,请处理事件的后续部分。开始想过使用同步对象,文件影射,消息等实现DLL函数到应用程序的通知,后来突然想到可不可以在应用程序端先写一个函数,等需要处理后续事宜的时候,在DLL里直接调用这个函数即可。
于是就动手,写了个回调函数的原形。在VC和 Delphi里都进行了测试 一:声明回调函数类型。 vc版 typedef int (WINAPI *PFCALLBACK)(int Param1,int Param2) ; Delph版 PFCALLBACK = function(Param1:integer;Param2:integer):integer;stdcall; 实际上是声明了一个返回值为int,传入参数为两个int的指向函数的指针。 由于C++和PASCAL编译器对参数入栈和函数返回的处理有可能不一致,把函数类型用WINAPI(WINAPI宏展开就是__stdcall)或stdcall统一修饰。 二:声明回调函数原形 声明函数原形 vc版 int WINAPI CBFunc(int Param1,int Param2); Delphi版 function CBFunc(Param1,Param2:integer):integer;stdcall; 以上函数为全局函数,如果要使用一个类里的函数作为回调函数原形,把该类函数声明为静态函数即可。 三: 回调函数调用调用者 调用回调函数的函数我把它放到了DLL里,这是一个很简单的VC生成的WIN32 DLL.并使用DEF文件输出其函数名 TestCallBack。实现如下: PFCALLBACK gCallBack=0; void WINAPI TestCallBack(PFCALLBACK Func) { if(Func==NULL)return; gCallBack=Func; DWORD ThreadID=0; HANDLE hThread = CreateThread( NULL, NULL, Thread1, LPVOID(0), &ThreadID ); return; }
此函数的工作把传入的 PFCALLBACK Func参数保存起来等待使用,并且启动一个线程。声明了一个函数指针PFCALLBACK gCallBack保存传入的函数地址。 四: 回调函数如何被使用:
TestCallBack函数被调用后,启动了一个线程,作为演示,线程人为的进行了延时处理,并且把线程运行的过程打印在屏幕上. 本段线程的代码也在DLL工程里实现 ULONG WINAPI Thread1(LPVOID Param) { TCHAR Buffer[256]; HDC hDC = GetDC(HWND_DESKTOP); int Step=1; MSG Msg; DWORD StartTick; //一个延时循环 for(;Step<200;Step++) { StartTick = GetTickCount(); /*这一段为线程交出部分运行时间以让系统处理其他事务*/ for(;GetTickCount()-StartTick<10;) { if(PeekMessage(&Msg,NULL,0,0,PM_NOREMOVE) ) { TranslateMessage(&Msg); DispatchMessage(&Msg); } } /*把运行情况打印到桌面,这是vcbear调试程序时最喜欢干的事情*/ sprintf(Buffer,"Running %04d",Step); if(hDC!=NULL) TextOut(hDC,30,50,Buffer,strlen(Buffer)); } /*延时一段时间后调用回调函数*/ (*gCallback)(Step,1); /*结束*/ ::ReleaseDC (HWND_DESKTOP,hDC); return 0; } 五:万事具备 使用vc和Delphi各建立了一个工程,编写回调函数的实现部分 VC版 int WINAPI CBFunc(int Param1,int Param2) { int res= Param1+Param2; TCHAR Buffer[256]=""; sprintf(Buffer,"callback result = %d",res); MessageBox(NULL,Buffer,"Testing",MB_OK); //演示回调函数被调用 return res; }
Delphi版 function CBFunc(Param1,Param2:integer):integer; begin result:= Param1+Param2; TForm1.Edit1.Text:=inttostr(result); / /演示回调函数被调用 end; 使用静态连接的方法连接DLL里的出口函数 TestCallBack,在工程里添加 Button( 对于Delphi的工程,还需要在Form1上放一个Edit控件,默认名为Edit1)。 响应ButtonClick事件调用 TestCallBack TestCallBack(CBFunc) //函数的参数CBFunc为回调函数的地址 函数调用创建线程后立刻返回,应用程序可以同时干别的事情去了。现在可以看到屏幕上不停的显示字符串,表示dll里创建的线程运行正常。一会之后,线程延时部分结束结束,vc的应用程序弹出MessageBox,表示回调函数被调用并显示根据Param1,Param2运算的结果,Delphi的程序edit控件里的文本则被改写成Param1,Param2 的运算结果。 可见使用回调函数的编程模式,可以根据不同的需求传递不同的回调函数地址,或者定义各种回调函数的原形(同时也需要改变使用回调函数的参数和返回值约定),实现多种回调事件处理,可以使程序的控制灵活多变,也是一种高效率的,清晰的程序模块之间的耦合方式。在一些异步或复杂的程序系统里尤其有用 -- 你可以在一个模块(如DLL)里专心实现模块核心的业务流程和技术功能,外围的扩展的功能只给出一个回调函数的接口,通过调用其他模块传递过来的回调函数地址的方式,将后续处理无缝地交给另一个模块,随它按自定义的方式处理。 本文的例子使用了在DLL里的多线程延时后调用回调函数的方式,只是为了突出一下回调函数的效果,其实只要是在本进程之内,都可以随你高兴可以把函数地址传递来传递去,当成回调函数使用。 这样的编程模式原理非常简单单一:就是把函数也看成一个指针一个地址来调用,没有什么别的复杂的东西,仅仅是编程里的一个小技巧。至于回调函数模式究竟能为你带来多少好处,就看你是否使用,如何使用这种编程模式了。
Q 编程工具: C++ BUILDER 3.0 操作系统: WIN98 我想在C++ 中使用回调函数,请问它的内在机制如何,另外怎么定义。我用DialogBox函数时,如何使用回调函数? 它和钩子函数有何不同?多谢指教!!!拜托!!!
A
使用回调函数实际上就是在调用某个函数(通常是API函数)时,将自己的一个函数(这个函数为回调函数)的地址作为参数传递给那个函数。而那个函数在需要的时候,利用传递的地址调用回调函数,这时你可以利用这个机会在回调函数中处理消息或完成一定的操作。至于如何定义回调函数,跟具体使用的API函数有关,一般在帮助中有说明回调函数的参数和返回值等。C++中一般要求在回调函数前加CALLBACK,这主要是说明该函数的调用方式。DialogBox的回调函数实际上是个窗口过程,用来处理所有消息。其定义为: BOOL CALLBACK DialogProc( HWND hwndDlg, // handle of dialog box UINT uMsg, // message WPARAM wParam, // first message parameter LPARAM lParam // second message parameter ); 在Win32 API中有详细说明。一般使用C++ Builder或MFC的往往没有使用SDK编程的经验,建议找一些SDK编程的书看一下,否则很难理解如何使用窗口过程。 至于钩子函数,只是回调函数的一个特例。习惯上把与SetWindowsHookEx函数一起使用的回调函数称为钩子函数。也有人把利用VirtualQueryEx安装的函数称为钩子函数,不过这种叫法不太流行。 frank的意见: 我对回调函数的理解虽然粗浅,但是我觉得会让人更容易理解:回调函数就相当于一个中断处理函数,由系统在符合你设定的条件时自动调用。为此,你需要做三件事:1,声明;2,定义;3,设置触发条件,就是在你的函数中把你的回调函数名称转化为地址作为一个参数,以便于系统调用。 声明和定义时应注意:回调函数由系统调用,所以可以认为它属于WINDOWS系统。不要把它当作你的某个类的成员函数。 ping的意见: frank说:回调函数属于WINDOWS系统。我觉得不应该说回调函数是属于系统的。应该说是程序把这段代码的触发交由系统来做。而这种做法是WINDOWS提供的处理机制吧,因为消息是系统一手掌握着的,由系统来调用我们的程序对消息的处理部分,这样子会比较方便。不然我们又得花力气去读消息列表了。(不知道我说的对不对,接触系统还不深,请高手指教哦)
今天学习了一下回调函数,编了一个小型demo.如下。 但是还没想出来在实际应用中能起到什么作用。听说是很有用的。 我觉得他的实用之处就是能传递函数指针。至少目前我是这么认为的。
VC++ 6.0通过 typedef int(*CallBack)(char *p) ; // 声明CallBack 类型的函数指针 int A(char *p){ AfxMessageBox("A"); AfxMessageBox(p); return 0; } int B(CallBack lpCall,char *p) { AfxMessageBox("B"); AfxMessageBox(p); lpCall(p); // 借助回调完成的功能 ,也就是A函数来处理的。 return 0; } void hello::OnButton9() { char *p = "hello!"; B(A,p); }
软件模块之间总是存在着一定的接口,从调用方式上,可以把他们分为三类:同步调用、回调和异步调用。同步调用是一种阻塞式调用,调用方要等待对方执行完毕才返回,它是一种单向调用;回调是一种双向调用模式,也就是说,被调用方在接口被调用时也会调用对方的接口;异步调用是一种类似消息或事件的机制,不过它的调用方向刚好相反,接口的服务在收到某种讯息或发生某种事件时,会主动通知客户方(即调用客户方的接口)。回调和异步调用的关系非常紧密,通常我们使用回调来实现异步消息的注册,通过异步调用来实现消息的通知。同步调用是三者当中最简单的,而回调又常常是异步调用的基础,因此,下面我们着重讨论回调机制在不同软件架构中的实现。
1 什么是回调
软件模块之间总是存在着一定的接口,从调用方式上,可以把他们分为三类:同步调用、回调和异步调用。同步调用是一种阻塞式调用,调用方要等待对方执行完毕才返回,它是一种单向调用;回调是一种双向调用模式,也就是说,被调用方在接口被调用时也会调用对方的接口;异步调用是一种类似消息或事件的机制,不过它的调用方向刚好相反,接口的服务在收到某种讯息或发生某种事件时,会主动通知客户方(即调用客户方的接口)。回调和异步调用的关系非常紧密,通常我们使用回调来实现异步消息的注册,通过异步调用来实现消息的通知。同步调用是三者当中最简单的,而回调又常常是异步调用的基础,因此,下面我们着重讨论回调机制在不同软件架构中的实现。
对于不同类型的语言(如结构化语言和对象语言)、平台(Win32、JDK)或构架(CORBA、DCOM、WebService),客户和服务的交互除了同步方式以外,都需要具备一定的异步通知机制,让服务方(或接口提供方)在某些情况下能够主动通知客户,而回调是实现异步的一个最简捷的途径。
对于一般的结构化语言,可以通过回调函数来实现回调。回调函数也是一个函数或过程,不过它是一个由调用方自己实现,供被调用方使用的特殊函数。
在面向对象的语言中,回调则是通过接口或抽象类来实现的,我们把实现这种接口的类成为回调类,回调类的对象成为回调对象。对于象C++或Object Pascal这些兼容了过程特性的对象语言,不仅提供了回调对象、回调方法等特性,也能兼容过程语言的回调函数机制。
Windows平台的消息机制也可以看作是回调的一种应用,我们通过系统提供的接口注册消息处理函数(即回调函数),从而实现接收、处理消息的目的。由于Windows平台的API是用C语言来构建的,我们可以认为它也是回调函数的一个特例。
对于分布式组件代理体系CORBA,异步处理有多种方式,如回调、事件服务、通知服务等。事件服务和通知服务是CORBA用来处理异步消息的标准服务,他们主要负责消息的处理、派发、维护等工作。对一些简单的异步处理过程,我们可以通过回调机制来实现。
下面我们集中比较具有代表性的语言(C、Object Pascal)和架构(CORBA)来分析回调的实现方式、具体作用等。
|
2 过程语言中的回调(C)
2.1 函数指针
回调在C语言中是通过函数指针来实现的,通过将回调函数的地址传给被调函数从而实现回调。因此,要实现回调,必须首先定义函数指针,请看下面的例子:
void Func(char *s);// 函数原型 void (*pFunc) (char *);//函数指针 |
可以看出,函数的定义和函数指针的定义非常类似。
一般的化,为了简化函数指针类型的变量定义,提高程序的可读性,我们需要把函数指针类型自定义一下。
typedef void(*pcb)(char *); |
回调函数可以象普通函数一样被程序调用,但是只有它被当作参数传递给被调函数时才能称作回调函数。
被调函数的例子:
void GetCallBack(pcb callback) { /*do something*/ } 用户在调用上面的函数时,需要自己实现一个pcb类型的回调函数: void fCallback(char *s) { /* do something */ } 然后,就可以直接把fCallback当作一个变量传递给GetCallBack, GetCallBack(fCallback); |
如果赋了不同的值给该参数,那么调用者将调用不同地址的函数。赋值可以发生在运行时,这样使你能实现动态绑定。
|
2.2 参数传递规则
到目前为止,我们只讨论了函数指针及回调而没有去注意ANSI C/C++的编译器规范。许多编译器有几种调用规范。如在Visual C++中,可以在函数类型前加_cdecl,_stdcall或者_pascal来表示其调用规范(默认为_cdecl)。C++ Builder也支持_fastcall调用规范。调用规范影响编译器产生的给定函数名,参数传递的顺序(从右到左或从左到右),堆栈清理责任(调用者或者被调用者)以及参数传递机制(堆栈,CPU寄存器等)。
将调用规范看成是函数类型的一部分是很重要的;不能用不兼容的调用规范将地址赋值给函数指针。例如:
// 被调用函数是以int为参数,以int为返回值 __stdcall int callee(int); // 调用函数以函数指针为参数 void caller( __cdecl int(*ptr)(int)); // 在p中企图存储被调用函数地址的非法操作 __cdecl int(*p)(int) = callee; // 出错 |
指针p和callee()的类型不兼容,因为它们有不同的调用规范。因此不能将被调用者的地址赋值给指针p,尽管两者有相同的返回值和参数列
2.3 应用举例
C语言的标准库函数中很多地方就采用了回调函数来让用户定制处理过程。如常用的快速排序函数、二分搜索函数等。
快速排序函数原型:
void qsort(void *base, size_t nelem, size_t width, int (_USERENTRY *fcmp)(const void *, const void *)); 二分搜索函数原型: void *bsearch(const void *key, const void *base, size_t nelem, size_t width, int (_USERENTRY *fcmp)(const void *, const void *)); |
其中fcmp就是一个回调函数的变量。
下面给出一个具体的例子:
#include <stdio.h> #include <stdlib.h> int sort_function( const void *a, const void *b); int list[5] = { 54, 21, 11, 67, 22 }; int main(void) { int x; qsort((void *)list, 5, sizeof(list[0]), sort_function); for (x = 0; x < 5; x++) printf("%i\n", list[x]); return 0; } int sort_function( const void *a, const void *b) { return *(int*)a-*(int*)b; } |
2.4 面向对象语言中的回调(Delphi)
Dephi与C++一样,为了保持与过程语言Pascal的兼容性,它在引入面向对象机制的同时,保留了以前的结构化特性。因此,对回调的实现,也有两种截然不同的模式,一种是结构化的函数回调模式,一种是面向对象的接口模式。
2.4.1 回调函数
回调函数类型定义:
type TCalcFunc=function (a:integer;b:integer):integer; |
按照回调函数的格式自定义函数的实现,如
function Add(a:integer;b:integer):integer begin result:=a+b; end; function Sub(a:integer;b:integer):integer begin result:=a-b; end; |
回调的使用
function Calc(calc:TcalcFunc;a:integer;b:integer):integer |
下面,我们就可以在我们的程序里按照需要调用这两个函数了
c:=calc(add,a,b);//c=a+b c:=calc(sub,a,b);//c=a-b |
2.4.2 回调对象
什么叫回调对象呢,它具体用在哪些场合?首先,让我们把它与回调函数对比一下,回调函数是一个定义了函数的原型,函数体则交由第三方来实现的一种动态应用模式。要实现一个回调函数,我们必须明确知道几点:该函数需要那些参数,返回什么类型的值。同样,一个回调对象也是一个定义了对象接口,但是没有具体实现的抽象类(即接口)。要实现一个回调对象,我们必须知道:它需要实现哪些方法,每个方法中有哪些参数,该方法需要放回什么值。
因此,在回调对象这种应用模式中,我们会用到接口。接口可以理解成一个定义好了但是没有实现的类,它只能通过继承的方式被别的类实现。Delphi中的接口和COM接口类似,所有的接口都继承与IInterface(等同于IUnknow),并且要实现三个基本的方法QueryInterface, _AddRef, 和_Release。
· 定义一个接口
type IShape=interface(IInterface) procedure Draw; end |
· 实现回调类
type TRect=class(TObject,IShape) protected function QueryInterface(const IID: TGUID; out Obj): HResult; stdcall; function _AddRef: Integer; stdcall; function _Release: Integer; stdcall; public procedure Draw; end; type TRound=class(TObject,IShape) protected function QueryInterface(const IID: TGUID; out Obj): HResult; stdcall; function _AddRef: Integer; stdcall; function _Release: Integer; stdcall; public procedure Draw; end; |
· 使用回调对象
procedure MyDraw(shape:IShape); var shape:IShape; begin shape.Draw; end; |
如果传入的对象为TRect,那么画矩形;如果为TRound,那么就为圆形。用户也可以按照自己的意图来实现IShape接口,画出自己的图形:
MyDraw(Trect.Create); MyDraw(Tround.Create); |
2.4.3 回调方法
回调方法(Callback Method)可以看作是回调对象的一部分,Delphi对windows消息的封装就采用了回调方法这个概念。在有些场合,我们不需要按照给定的要求实现整个对象,而只要实现其中的一个方法就可以了,这是我们就会用到回调方法。
回调方法的定义如下:
TNotifyEvent = procedure(Sender: TObject) of object; TMyEvent=procedure(Sender:Tobject;EventId:Integer) of object; |
TNotifyEvent 是Delphi中最常用的回调方法,窗体、控件的很多事件,如单击事件、关闭事件等都是采用了TnotifyEvent。回调方法的变量一般通过事件属性的方式来定义,如TCustomForm的创建事件的定义:
property OnCreate: TNotifyEvent read FOnCreate write FOnCreate stored IsForm; |
我们通过给事件属性变量赋值就可以定制事件处理器。
用户定义对象(包含回调方法的对象):
type TCallback=Class procedure ClickFunc(sender:TObject); end; procedure Tcallback.ClickFunc(sender:TObject); begin showmessage('the caller is clicked!'); end; |
窗体对象:
type TCustomFrm=class(TForm) public procedure RegisterClickFunc(cb:procedure(sender:Tobject) of object); end; procedure TcustomFrm..RegisterClickFunc(cb:TNotifyEvent); begin self.OnClick=cb; end; |
使用方法:
var frm:TcustomFrm; begin frm:=TcustomFrm.Create(Application); frm.RegisterClickFunc(Tcallback.Create().ClickFunc); end; |
3 回调在分布式计算中的应用(CORBA)
3.1 回调接口模型
CORBA的消息传递机制有很多种,比如回调接口、事件服务和通知服务等。回调接口的原理很简单,CORBA客户和服务器都具有双重角色,即充当服务器也是客户客户。
回调接口的反向调用与正向调用往往是同时进行的,如果服务端多次调用该回调接口,那么这个回调接口就变成异步接口了。因此,回调接口在CORBA中常常充当事件注册的用途,客户端调用该注册函数时,客户函数就是回调函数,在此后的调用中,由于不需要客户端的主动参与,该函数就是实现了一种异步机制。
从CORBA规范我们知道,一个CORBA接口在服务端和客户端有不同的表现形式,在客户端一般使用桩(Stub)文件,服务端则用到框架(Skeleton)文件,接口的规格采用IDL来定义。而回调函数的引入,使得服务端和客户端都需要实现一定的桩和框架。下面是回调接口的实现模型:
3.1.1 范例
下面给出了一个使用回调的接口文件,服务端需要实现Server接口的框架,客户端需要实现CallBack的框架:
module cb { interface CallBack; interface Server; interface CallBack { void OnEvent(in long Source,in long msg); }; interface Server { long RegisterCB(in CallBack cb); void UnRegisterCB(in long hCb); }; }; |
客户端首先通过同步方式调用服务端的接口RegistCB,用来注册回调接口CallBack。服务端收到该请求以后,就会保留该接口引用,如果发生某种事件需要向客户端通知的时候就通过该引用调用客户方的OnEvent函数,以便对方及时处理。
本文源码 下载。
信号和回调函数的原理
在 2.0 版,信号系统已从 GTK 移到 GLib,因此在函数和类型的说明中有前缀 "g_" 而不是 "gtk_"。我们不打算介绍 GLib 2.0 信号系统相对 GTK 1.2 信号系统扩展的细节。 |
在我们详细分析 helloworld 程序之前,我们会讨论信号和回调函数。GTK 是一个事件驱动的工具包,意味着它会等在 gtk_main() 那里,直到下一个事件发生,才把控制权传给适当的函数。
控制权的传递是使用“信号”的办法来完成的。(注意这里的信号并不等同于 Unix 系统里的信号,并且也不是用它们实现的,虽然使用的术语是一样的。) 当一个事件发生时,如按一下鼠标键,所按的构件会“发出”适当的信号。这就是 GTK 的工作机制。有所有构件都继承的信号,如 "destroy",有构件专有的信号,如开关 (toggle) 按钮发出的 "toggled" 信号。
要使一个按钮执行一个动作,我们需设置信号和信号处理函数之间的连接。可以这样使用函数来设置连接:
gulong g_signal_connect( gpointer *object, const gchar *name, GCallback func, gpointer func_data ); |
第一个参数是要发出信号的构件,第二个参数是你想要连接的信号的名称,第三个参数是信号被捕获时所要调用的函数,第四个参数是你想传递给这个函数的数据。
第三个参数指定的函数叫做回调函数,一般为下面的形式:
void callback_func( GtkWidget *widget, gpointer callback_data ); |
第一个参数是一个指向发出信号的构件的指针,第二个参数是一个指向数据的指针,就是上面 g_signal_connect() 函数的最后一个参数传进来的数据。
注意上面回调函数的声明只是一般的形式,有些构件的特殊信号会用不同的调用参数。
另一个在 helloworld 示例中使用的调用,是:
gulong g_signal_connect_swapped( gpointer *object, const gchar *name, GCallback func, gpointer *slot_object ); |
g_signal_connect_swapped() 和 g_signal_connect() 相同,只是回调函数只用一个参数,一个指向 GTK 对象的指针。所以当使用这个函数连接信号时,回调函数应该是这样的形式
void callback_func( GtkObject *object ); |
这个对象通常是一个构件。然而我们一般不用函数 g_signal_connect_swapped() 设置回调。它们常用来调用一个只接受一个单独的构件或者对象作为参数的 GTK 函数,如同我们的 helloworld 示例中那样。
拥有两个函数来设置信号连接的目的只是为了允许回调函数有不同数目的参数。GTK 库中许多函数仅接受一个单独的构件指针作为其参数,所以对于这些函数你要用 g_signal_connect_swapped(),然而对你自己定义的函数,你可能需要附加的数据提供给你的回调函数。