当前位置: 代码迷 >> 综合 >> CallBack函数及其控制
  详细解决方案

CallBack函数及其控制

热度:27   发布时间:2024-01-19 21:59:02.0

回调函数是一个很有用,也很重要的概念。当发生某种事件时,系统或其他函数将会自动调用你定义的一段函数。回调函数在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里的多线程延时后调用回调函数的方式,只是为了突出一下回调函数的效果,其实只要是在本进程之内,都可以随你高兴可以把函数地址传递来传递去,当成回调函数使用。  
   
   这样的编程模式原理非常简单单一:就是把函数也看成一个指针一个地址来调用,没有什么别的复杂的东西,仅仅是编程里的一个小技巧。至于回调函数模式究竟能为你带来多少好处,就看你是否使用,如何使用这种编程模式了。  
   
  //----------------------------------------------------------------------  
  又一篇关于回调函数的文章  
   
  回调函数,就是由你自己写的。你需要调用另外一个函数,而这个函数的其中一个参数,就  
  是你的这个回调函数名。这样,系统在必要的时候,就会调用你写的回调函数,这样你就可  
  以在回调函数里完成你要做的事。  
   
  capvideostreamcallback   这个回调函数,我没有做过,看了一下help,应该是通过发送消息  
  wm_cap_set_callback_videostream,来设置的,或者调用宏capsetcallbackonvideostream  
  来完成的。这样设定之后,系统在进行图像捕捉的过程中,就会自动调用你写的回调函数。  
   
  这个回调函数的函数体需要你自已来写,然后在另一函数中调用,即是说:  
  lresult   callback   capvideostreamcallback(hwnd   hwnd,lpvideohdr   lpvhdr)  
  {  
    ........  
  }  
  //在另一函数中调用它时,通过从a里面传递过来的foo的地址调用foo,通知a发生了什么事情,让a作出相应反应。  
          那么我们就把foo称为回调函数。  
   
          “这个回调函数不是vfw.h中声明的么,“  
          ----那是声明了回调函数原型,是告诉你传递进来的回调函数必须和它定义的原型保持一致。  
   
          ”为什么要自己写函数体呢?“  
          ----比如在上面模块b里面,它只知道当event发生时,向模块a发出通知,具体怎么回应这个事件,不是b所关心的,也不是b所能预料到的。  
          你站在a的角度上思考,当然要你自己作出对event的反应,也就是你要自己写函数体。  
   
          你如果明白了c++里面的函数指针,就很容易理解回调函数了。  
   
  "不知道系统调用后有什么结果,或者我怎么利用这个结果啊"  
  ---如果你向系统传递一个回调函数地址,那么你的程序就相当于上面我说的模块a,系统就相当于模块b,系统只是调用你的函数,它根本不可能知道会有什么结果。  
        你怎么利用这个结果,看你是怎么定义这个回调函数的。              
  回调函数和回调机制是不同的概念,。,,函数是被调用的,但是回调机制在不同的语言中不都是以函数指针来实现的。。。。比如c#...一般的在windows   api   中,会调都是使用函数指针实现的。。。  
   
   
   
 

发表者:seu_why
函数调用方式  
     
  我们知道在进行函数调用时,有几种调用方法,主要分为c式,pascal式.在c和c++中  
  c式调用是缺省的,类的成员函数缺省调用为_stdcall。二者是有区别的,下面我们用  
  实例说明一下:  
   
  1.   __cdecl   :c和c++缺省调用方式  
  例子:    
  void   input(   int   &m,int   &n);/*相当于void   __cdecl   input(int   &m,int   &n);*/  
  以下是相应的汇编代码:  
  00401068   lea   eax,[ebp-8]   ;取[ebp-8]地址(ebp-8),存到eax  
  0040106b   push   eax   ;然后压栈  
  0040106c   lea   ecx,[ebp-4]   ;取[ebp-4]地址(ebp-4),存到ecx  
  0040106f   push   ecx   ;然后压栈  
  00401070   call   @ilt+5(input)   (0040100a);然后调用input函数  
  00401075   add   esp,8   ;恢复栈  
     
          从以上调用input函数的过程可以看出:在调用此函数之前,首先压栈ebp-8,然  
  后压栈ebp-4,然后调用函数input,最后input函数调用结束后,利用esp+8恢复栈。由  
  此可见,在c语言调用中默认的函数修饰_cdecl,由主调用函数进行参数压栈并且恢复  
  堆栈。  
  下面看一下:地址ebp-8和ebp-4是什么?  
          在vc的view下选debug   windows,然后选registers,显示寄存器变量值,然后在  
  选debug   windows下面的memory,输入ebp-8的值和ebp-4的值(或直接输入ebp-8和-4),  
  看一下这两个地址实际存储的是什么值,实际上是变量   n   的地址(ebp-8),m的地址  
  (ebp-4),由此可以看出:在主调用函数中进行实参的压栈并且顺序是从右到左。另外,  
  由于实参是相应的变量的引用,也证明实际上引用传递的是变量的地址(类似指针)。  
  总结:在c或c++语言调用中默认的函数修饰_cdecl,由主调用函数进行参数压栈并且  
  恢复堆栈,实参的压栈顺序是从右到左,最后由主调函数进行堆栈恢复。由于主调用  
  函数管理堆栈,所以可以实现变参函数。另外,命名修饰方法是在函数前加一个下划  
  线(_).  
   
  2.   winapi   (实际上就是pascal,callback,_stdcall)  
  例子:    
  void   winapi   input(   int   &m,int   &n);  
  看一下相应调用的汇编代码:  
  00401068   lea   eax,[ebp-8]  
  0040106b   push   eax  
  0040106c   lea   ecx,[ebp-4]  
  0040106f   push   ecx  
  00401070   call   @ilt+5(input)   (0040100a)  
     
          从以上调用input函数的过程可以看出:在调用此函数之前,首先压栈ebp-8,然  
  后压栈ebp-4,然后调用函数input,在调用函数input之后,没有相应的堆栈恢复工作  
  (为其它的函数调用,所以我没有列出)  
  下面再列出input函数本身的汇编代码:(实际此函数不大,但做汇编例子还是大了些,  
  大家可以只看前和后,中间代码与此例子无关)    
  39:   void   winapi   input(   int   &m,int   &n)  
  40:   {  
  00401110   push   ebp  
  00401111   mov   ebp,esp  
  00401113   sub   esp,48h  
  00401116   push   ebx  
  00401117   push   esi  
  00401118   push   edi  
  00401119   lea   edi,[ebp-48h]  
  0040111c   mov   ecx,12h  
  00401121   mov   eax,0cccccccch  
  00401126   rep   stos   dword   ptr   [edi]  
  41:   int   s,i;  
  42:  
  43:   while(1)  
  00401128   mov   eax,1  
  0040112d   test   eax,eax  
  0040112f   je   input+0c1h   (004011d1)  
  44:   {  
  45:   printf("/nplease   input   the   first   number   m:");  
  00401135   push   offset   string   "/nplease   input   the   first   number   m"...   (004260b8)  
  0040113a   call   printf   (00401530)  
  0040113f   add   esp,4  
  46:   scanf("%d",&m);  
  00401142   mov   ecx,dword   ptr   [ebp+8]  
  00401145   push   ecx  
  00401146   push   offset   string   "%d"   (004260b4)  
  0040114b   call   scanf   (004015f0)  
  00401150   add   esp,8  
  47:  
  48:   if   (   m=   s   )  
  004011b3   mov   eax,dword   ptr   [ebp+8]  
  004011b6   mov   ecx,dword   ptr   [eax]  
  004011b8   cmp   ecx,dword   ptr   [ebp-4]  
  004011bb   jl   input+0afh   (004011bf)  
  57:   break;  
  004011bd   jmp   input+0c1h   (004011d1)  
  58:   else  
  59:   printf("   m   <   n*(n+1)/2,please   input   again!/n");  
  004011bf   push   offset   string   "   m   <   n*(n+1)/2,please   input   agai"...   (00426060)  
  004011c4   call   printf   (00401530)  
  004011c9   add   esp,4  
  60:   }  
  004011cc   jmp   input+18h   (00401128)  
  61:  
  62:   }  
  004011d1   pop   edi  
  004011d2   pop   esi  
  004011d3   pop   ebx  
  004011d4   add   esp,48h  
  004011d7   cmp   ebp,esp  
  004011d9   call   __chkesp   (004015b0)  
  004011de   mov   esp,ebp  
  004011e0   pop   ebp  
  004011e1   ret   8  
     
          最后,我们看到在函数末尾部分,有ret   8,明显是恢复堆栈,由于在32位c++中,  
  变量地址为4个字节(int也为4个字节),所以弹栈两个地址即8个字节。  
  由此可以看出:在主调用函数中负责压栈,在被调用函数中负责恢复堆栈。因此不能  
  实现变参函数,因为被调函数不能事先知道弹栈数量,但在主调函数中是可以做到的,  
  因为参数数量由主调函数确定。  
  下面再看一下,ebp-8和ebp-4这两个地址实际存储的是什么值,ebp-8地址存储的是n    
  的值,ebp   -4存储的是m的值。说明也是从右到左压栈,进行参数传递。  
   
          总结:在主调用函数中负责压栈,在被调用函数中负责弹出堆栈中的参数,并且  
  负责恢复堆栈。因此不能实现变参函数,参数传递是从右到左。另外,命名修饰方法  
  是在函数前加一个下划线(_),在函数名后有符号(@),在@后面紧跟参数列表中的参数  
  所占字节数(10进制),如:void   input(int   &m,int   &n),被修饰成:_input@8  
  对于大多数api函数以及窗口消息处理函数皆用   callback   ,所以调用前,主调函数会  
  先压栈,然后api函数自己恢复堆栈。  
   
  如:    
  push   edx  
  push   edi  
  push   eax  
  push   ebx  
  call   getdlgitemtexta  
   
  //-------------------------------------------------------------------------  
  rundll32.exe   的回调函数在msdn上的申明为      
  void   callback   entrypoint(  
      hwnd   hwnd,                 //   handle   to   owner   window  
      hinstance   hinst,     //   instance   handle   for   the   dll  
      lptstr   lpcmdline,   //   string   the   dll   will   parse  
      int   ncmdshow             //   show   state  
  );  
   
  但是,当我们的dll工程设置了?]认调用规则为stdcall时就会出错,  
  因此,实际定义这种导出函数时,应该这样申明  
  extern   "c"   __declspec(dllexport)  
  void     __cdecl   entrypoint(  
      hwnd   hwnd,                 //   handle   to   owner   window  
      hinstance   hinst,     //   instance   handle   for   the   dll  
      lptstr   lpcmdline,   //   string   the   dll   will   parse  
      int   ncmdshow             //   show   state  
  )  
   

关于CALLBACK函数2006-12-28 13:53
 callback函数一般不需要你去直接调用。它一般用作参数去调用某些

API函数。如使用SetTimer函数时,如果你不想让窗口来响应WM_TIMER消息

可以用一个callback函数为参数去调用该函数,当定时器事件发生时,Windows

将自动调用你提供的这个callback函数来响应WM_TIMER消息。

  再有,声音采集的时候也可以使用callback函数。为打开波形声音输入设备

而调用waveInOpen()时,可以把一个callback函数作为参数。调用waveInStart( )

启动录音过程后,当指定的波形声音准备好后,Windows系统将被通知去调用这个

callback函数,进行数据的后处理,如写入文件等。显然,你在启动录音过程后

无须使程序挂起去等待声音数据准备好,而可以接着执行以下的代码。

  实际上callback函数给你提供了一种异步的程序执行逻辑。你事先准备好

要执行的代码,并通过API调用告诉Windows,而当相应事件发生时,Windows将被告知