posts - 4, comments - 119, trackbacks - 1, articles - 0
  博客园 :: 首页 :: 新随笔 :: 联系 :: 订阅 订阅 :: 管理
写在最前面:
        无论是用什么编程语言编写应用程序,都会涉及到函数调用之间的问题。而调用过程可以分为两种,一种是主动请求调用,一种是被动等待调用。这也就是我们常说的调用与回调。下面我将说明DotNet(C#)与ISO C++关于函数回调的实现分析。

一、DotNet(C#)函数回调。
        在DotNet中实现函数调用是通过委托(delegate)实现的,首先你要声明委托原型:
    
delegate void Notify( int newValue );

      这样就声明了一个委托,那到底什么是委托呢?其实委托就是一个回调函数(更确切的说委托是一个安全的函数指针)。当需要回调的时候。可以调用委托的成员函数 Invoke 就可以实现调用你设置的回调函数。这时Invoke会自动根据你声明的委托形式进行调用。在这里我们举一个例子,压力计、报警器的例子:

当压力计的压力指数变化的时候,报警器会报警,并打印出变化的压力值。代码如下
 1using System;
 2using System.Collections.Generic;
 3using System.Text;
 4
 5namespace ConsoleApplication2
 6{
 7    // 这里声明委托
 8    public delegate void Notify( int newValue );
 9
10    // 压力计
11    class Piezometer
12    {
13        // 压力值
14        private int m_PressureNumber;
15        public int PressureNumber
16        {
17            get
18            {
19                // 返回当前压力值
20                return this.m_PressureNumber;
21            }

22            set
23            {
24                // 设置新的压力值
25                this.m_PressureNumber = value;
26                // 判断是否有人注册该事件,如果有就调用并传入新的压力值。
27                if (OnPressureChanged != null)
28                {
29                    // 这里就是DotNet框架实现的委托好处,它可以根据
30                    // 声明的形式自动匹配调用的参数表和返回值。
31
32                    // 调用回调事件,将新的压力值传入
33                    OnPressureChanged.Invoke(value);
34                }

35            }

36        }

37
38        // 声明一个事件,当压力值变化的时候触发该事件
39        public event Notify OnPressureChanged;
40    }

41
42    // 报警器
43    class Alerter
44    {
45        // 设置监听的压力计
46        public void Listen(Piezometer piezometer)
47        {
48            // 注册压力计压力变化事件
49            piezometer.OnPressureChanged += new Notify(OnChanged);
50        }

51        // 这里就是压力计变化后调用的函数
52        public void OnChanged(int newValue)
53        {
54            // 打印出新的压力值
55            Console.WriteLine(string.Format("New PressureNumber is {0}.", newValue));
56        }

57    }

58
59    class Program
60    {
61        static void Main(string[] args)
62        {
63            Alerter alerter = new Alerter();
64            Piezometer piezometer = new Piezometer();
65
66            // 安装压力计,进行监听
67            alerter.Listen(piezometer);
68
69            // 设置新的压力值,报警器就会打印出新的压力值。
70            piezometer.PressureNumber = 10;
71        }

72    }

73}

74

根据上面的代码我们可以实现自己的自定义事件。(感叹:DotNet框架真是太便利了,声明委托之后,委托的调用方法会自动变成声明的形式。C++就不支持这种操作。下面我会讲一下C++的实现方法。)
在这里我们用Reflector反编译一下Delegate类,该类是委托类型的基类。然而,只有系统和编译器可以显式地从 Delegate 类或 MulticastDelegate 类派生。此外,还不允许从委托类型派生新类型。Delegate 类不是委托类型,该类用于派生委托类型。而我们实现的都是MulticastDelegate派生类型,这样就可以产生一个委托多播的类型。最基本的实现是

1protected virtual object DynamicInvokeImpl(object[] args)
2{
3    RuntimeMethodHandle methodHandle = new RuntimeMethodHandle(this.GetInvokeMethod());
4    RuntimeMethodInfo methodBase = (RuntimeMethodInfo) RuntimeType.GetMethodBase(Type.GetTypeHandle(this), methodHandle);
5    return methodBase.Invoke(this, BindingFlags.Default, null, args, nulltrue);
6}

7

当委托被调用时会产生一个运行时方法对象。并通过运行时对象调用

1[MethodImpl(MethodImplOptions.InternalCall), DebuggerStepThrough, DebuggerHidden]
2private extern object _InvokeMethodFast(object target, object[] arguments, ref SignatureStruct sig, MethodAttributes methodAttributes, RuntimeTypeHandle typeOwner);

通知VM进行系统执行方法体。而在多播委托里包含一个_invocationList实现了保存多个委托,使之内部可以循环调用进行广播。


二、ISO C++事件回调。
      注意这里我指的是ISO C++实现的回调机制,他不依赖于任何操作系统。C++作为C语言的扩展(C++的Fans不要生气,毕竟C++在企业级快速开发比Java、DotNet要逊色一些),还是举上面的例子实现代码如下:

event.hpp(点击下载代码)

  1#ifndef __EVENT_HPP
  2#define __EVENT_HPP
  3
  4#include <vector>
  5
  6using std::vector;
  7
  8class EmptyType {};
  9
 10template< typename EventFunctionPtrType >
 11class EventObject
 12{
 13public:
 14    EventObject() :
 15        ObjectPtr( NULL ),
 16        EventFunctionPointerPtr( NULL )
 17    {
 18    }

 19
 20public:
 21    EmptyType*                ObjectPtr;
 22    EventFunctionPtrType*    EventFunctionPointerPtr;
 23}
;
 24
 25template< typename EventFunctionPtrType >
 26class Event
 27{
 28public:
 29    typedef vector< EventObject< EventFunctionPtrType > >                            EventList;
 30    typedef typename vector< EventObject< EventFunctionPtrType > >::iterator        EventIterator;
 31
 32public:
 33    Event()
 34    {
 35    }

 36
 37    virtual ~Event()
 38    {
 39        EventIterator iter;
 40        for( iter = m_EventList.begin();
 41            iter != m_EventList.end();
 42            ++iter )
 43        {
 44            delete iter->EventFunctionPointerPtr;
 45        }

 46    }

 47
 48    template< typename ObjectType,
 49            typename MemeberFunctionPtrType >
 50    void Bind( ObjectType* pObj, MemeberFunctionPtrType memberFunctionPtr )
 51    {
 52        MemeberFunctionPtrType* pf = new MemeberFunctionPtrType;
 53        *pf = memberFunctionPtr;
 54        EventObject< EventFunctionPtrType > eventObj;
 55        eventObj.ObjectPtr = (EmptyType*)pObj;
 56        eventObj.EventFunctionPointerPtr = (EventFunctionPtrType*)pf;
 57
 58        bool hasTheEvent = false;
 59        EventIterator iter;
 60        for( iter = m_EventList.begin();
 61            iter != m_EventList.end();
 62            ++iter )
 63        {
 64            if( iter->ObjectPtr == eventObj.ObjectPtr &&
 65                *iter->EventFunctionPointerPtr == *eventObj.EventFunctionPointerPtr )
 66            {
 67                hasTheEvent = true;
 68                break;
 69            }

 70        }

 71        if!hasTheEvent )
 72            m_EventList.push_back( eventObj );
 73        else
 74            delete eventObj.EventFunctionPointerPtr;
 75    }

 76
 77    template< typename ObjectType,
 78            typename MemeberFunctionPtrType >
 79    void UnBind( ObjectType* pObj, MemeberFunctionPtrType memberFunctionPtr )
 80    {
 81        MemeberFunctionPtrType* pf = new MemeberFunctionPtrType;
 82        *pf = memberFunctionPtr;
 83        EventObject< EventFunctionPtrType > eventObj;
 84        eventObj.ObjectPtr = (EmptyType*)pObj;
 85        eventObj.EventFunctionPointerPtr = (EventFunctionPtrType*)pf;
 86
 87        EventIterator iter;
 88        for( iter = m_EventList.begin();
 89            iter != m_EventList.end();
 90            ++iter )
 91        {
 92            if( iter->ObjectPtr == eventObj.ObjectPtr &&
 93                *iter->EventFunctionPointerPtr == *eventObj.EventFunctionPointerPtr )
 94            {
 95                delete iter->EventFunctionPointerPtr;
 96                m_EventList.erase( iter );
 97                break;
 98            }

 99        }

100        delete eventObj.EventFunctionPointerPtr;
101    }

102
103public:
104    EventList        m_EventList;
105}
;
106
107typedef EmptyType EventDelegater;
108
109#ifndef InvokeEvent
110#define InvokeEvent ((iter->ObjectPtr)->*(*iter->EventFunctionPointerPtr))        // Invoke the Event
111#endif
112
113#endif // __EVENT_HPP
114

 

example.cpp

 1// EventTest.cpp : 定义控制台应用程序的入口点。
 2//
 3
 4#include "stdafx.h"
 5#include "event.hpp"
 6
 7using std::cout;
 8using std::endl;
 9
10// 声明事件代理模型(类成员函数指针)
11typedef void ( EventDelegater::*NumberChanged )( int );
12
13// 压力计
14class Piezometer
15{
16public:
17    Piezometer();
18
19public:
20    void SetPressureNumber( int newVal ); // 设置压力值
21    int GetPressureNumber(); // 获取压力值
22
23    // 压力值变化事件
24    Event< NumberChanged > OnPressureChanged;
25
26private:
27    int m_PressureNumber;
28}
;
29
30Piezometer::Piezometer() : m_PressureNumber( 0 )
31{
32}

33
34void Piezometer::SetPressureNumber( int newVal )
35{
36    m_PressureNumber = newVal;
37
38    // 判断事件列表是否为空
39    if!OnPressureChanged.m_EventList.empty() )
40    {
41        // 循环事件列表
42        Event< NumberChanged >::EventIterator iter;
43        for( iter = OnPressureChanged.m_EventList.begin();
44            iter != OnPressureChanged.m_EventList.end();
45            ++iter )
46        {
47            // 调用事件
48            InvokeEvent( newVal );
49        }

50    }

51}

52
53int Piezometer::GetPressureNumber()
54{
55    return m_PressureNumber;
56}

57
58// 报警器
59class Alerter
60{
61public:
62    void Listen( Piezometer* );
63    void OnChanged( int newVal );
64}
;
65
66void Alerter::Listen( Piezometer* pObj )
67{
68    // 绑定成员函数到事件
69    pObj->OnPressureChanged.Bind( this&Alerter::OnChanged );
70}

71
72void Alerter::OnChanged( int newVal )
73{
74    cout << "New Pressure Number is " << newVal << "." << endl;
75}

76
77int _tmain(int argc, _TCHAR* argv[])
78{
79    Piezometer piezometer;
80    Alerter alerter;
81
82    alerter.Listen( &piezometer );
83    piezometer.SetPressureNumber( 10 );
84
85    return 0;
86}

87
88


上面的做法完全是按照面向对象设计的,这种函数回调方法并不是静态函数回调,而是对象方法回调。C++这种做法没有DotNet使用委托来的方便,因为C++回调类成员函数指针是不能转换为 void* ,必须转换成指针的指针,而且要引用一个EmptyType类,EmptyType类的更多应用可以参见《C++设计新思维》。
       回调成员函数是C++实现事件机制的方法之一,实现调用是通过 ((iter->ObjectPtr)->*(*iter->EventFunctionPointerPtr))( newVal ); 语句实现的,我在这里讲一下大体思路 class Event 实现了事件列表以及内存指针释放的功能,这样就可以实现事件的多播。class EventObject 实现了对象指针与该对象实现的成员函数的配对(当然也可以用STL中的pair),我们是通过C++操作符.*或是->*实现调用成员函数指针。iter->ObjectPtr保存了对象指针,iter->EventFunctionPointerPtr保存了对象成员函数指针的指针。因为C++不支持将一个类成员函数的指针转换成另一个类的成员函数指针,所以我在这里实现一个EmptyType类用于委托声明和指针转换。
      C++操作符.*或是->*实现调用成员函数指针,和回调静态函数有什么却别呢?
      面向对象是将一组方法和方法相关的数据绑定起来,当类成员函数调用的时候可以访问该类的成员变量,而访问变量是通过向成员函数传入该类的this指针,当然这个不用我们去实现,C++内部已经实现。在成员方法被调用时,该类的this指针会传到寄存器ECX。这样成员函数就可以通过ECX访问到成员变量了:

push 参数
mov ecx, this
call  成员函数

这样就实现了成员函数的调用,而静态函数不涉及到成员函数,所以函数内部所用到的数据都是通过 push 参数实现的。

三、Windows实现事件机制。
         使用Windows API实现事件机制主要是通过消息队列,通过GetMessage、PeekMessage创建消息循环,并调用DispatchMessage分发消息。这种实现事件机制,基本上是通过回调函数实现的,而不是通过回调成员函数实现的。具体方法可以参见MSDN。

写在最后:
        我个人比较喜欢第二种做法,原因如下:不依赖于操作系统,与平台无关。而且面向对象,在面向对象项目中比静态回调函数更适合。效率高,当使用Windows事件机制必须要创建窗体,使用消息循环,效率肯定比回调函数指针要低。但对于多线程没有Windows事件机制方便(一个线程要通知另一个线程,或是进程间通信无法实现,因为其始终在一个线程执行)。

Feedback

#1楼  回复 引用 查看   

2007-09-07 23:40 by 真水无香.chen      
C#部分写的很具体~~~:) 喜欢这样的风格

#2楼  回复 引用   

2007-09-08 10:17 by LeadNT[未注册用户]
保守的说,写的还算不错的,推荐一下~

#3楼  回复 引用   

2007-09-08 11:58 by 车车[未注册用户]
好很好!~踩两脚

#4楼  回复 引用 查看   

2007-09-09 09:26 by Dove.Net      
能让人学到一些东西的文章就是好文章

#5楼  回复 引用   

2007-09-10 09:26 by zhoucloud[未注册用户]
我想问下LZ,申明委托一般应该在什么文件中?
因为按照常理我们一般会把每个CLASS写成一个文件,但这个
public delegate void Notify( int newValue );
不属于任何一个类,而是属于这个命名空间,这就给我带来了困惑,到底这个delegate 应该写在哪?单独写个文件存放?
而且在CLASS前面定义这delegate ,有时会影响VS2005的提示功能,LZ能给个建议吗?

#6楼[楼主]  回复 引用 查看   

2007-09-10 10:17 by Aplo      
To:zhoucloud
这个问题我还真没有研究过。我想为了便于理解和使用还是把Delegate声明在你使用那个Delegate的类的旁边。这样找起来也方便,不过其实在什么地方都没有关系,你可以自己单写一个文件,把所有委托放在一个文件里,VS会自动提示(好像没有什么问题)。这样你就可以看出你声明的委托是否重复,我这里指的重复的意思是,委托的返回值和参数表都一样。

#7楼  回复 引用 查看   

2007-09-10 17:19 by 星星博客园      
short __stdcall dt_get_table_data(HANDLE hComDev,unsigned char pa_des,unsigned char pa_src,int fsid,unsigned long *pCount,short rec_size,unsigned char *buffer,void (*func_step)(int step));
这个该怎么样办呢?
最麻烦的是void (*func_step)(int step)该怎么样办呢?

#8楼[楼主]  回复 引用 查看   

2007-09-10 21:06 by Aplo      
To:星星博客园
我不太明白你的意思?什么怎么办?

#9楼  回复 引用   

2007-09-10 23:15 by 大菜[未注册用户]
好文章,辛苦

#10楼  回复 引用 查看   

2007-09-11 08:10 by 星星博客园      
对不起,我有点表达不太清楚,我想问一下,使用C#对short __stdcall dt_get_table_data(HANDLE hComDev,unsigned char pa_des,unsigned char pa_src,int fsid,unsigned long *pCount,short rec_size,unsigned char *buffer,void (*func_step)(int step));进行封装的时候,如何使用代理对void (*func_step)(int step)进行封装?请教一下,谢谢!

#11楼[楼主]  回复 引用 查看   

2007-09-11 11:23 by Aplo      
To:星星博客园
不好意思,我水平有限,还真不清楚非托管函数怎么回调托管函数,我试着写了一个但是有些问题,程序栈会报错,不知道是什么原因,可能是我用错了,没有仔细研究。在这里我说一下我的思路,看看对你是否有帮助。

extern "C"
{
void Do( void (*pf)(int) )
{
(*pf)(10);
}
}

我写了一个导出函数。在Dotnet项目写一个调用函数。
class Program
{
delegate void d(int i);

static void t(int i)
{
Console.WriteLine(string.Format("{0}", i));
}

static d p;
static void Main(string[] args)
{
p = new d(Program.t);
Do(p);
t(20);
}

[System.Runtime.InteropServices.DllImport("dll.dll")]
extern static void Do(d pf);
}
编译执行后报错,但是可以打印出10,但20打印不出来。
好像是调用时栈出了问题,我没有用汇编跟,所以不知道是那里的问题,不过从汇编角度看Program.t应该已经被jit编译过了,否则调用不会成功。打印不出值,但是传入参数可能有问题,把栈破坏了。结果在执行完是返回到未知地址,报错了,所以第二个t(20);打印不出来。

#12楼[楼主]  回复 引用 查看   

2007-09-11 11:27 by Aplo      
To:星星博客园
另外你可以用Reflector反编译一下类库,类库实现的功能都是DllImport Windows API或者是内部实现,你可以在MSDN上找一个API带回调的看看类库中怎么封装和实现调用的,可能会有答案。如果你实现了别忘了告诉我啊 :> 谢谢了,哈哈哈~~~

#13楼  回复 引用   

2007-09-13 12:26 by zwvista[未注册用户]
已进入标准的std::tr1::function以及基于此组件的Boost的signal已经基本解决了C++的函数回调问题,现在不再需要重复发明轮子了。

#14楼  回复 引用   

2007-09-17 13:34 by John1[未注册用户]
C++实现Delegate部分相当不错。

#15楼[楼主]  回复 引用 查看   

2007-09-17 21:19 by Aplo      
To:John1
谢谢,有机会多交流交流

To:zwvista
我觉得function用起来比较复杂,所以自己写了一个,哈哈

#16楼  回复 引用   

2007-09-24 14:17 by flyman[未注册用户]
@星星博客园
我有一段时间没有用C#了,所以说的不一定正确。对于这种封装可以用委托来实现:
//void (*func_step)(int step))
语法不一定能通过,你可以适当修改:
PUBLIC DELEGATE void callfun(int step);
void function1(int step)
{

}
callfun= new callfun(function1);
IntPtr funprt=GetFunctionPointerForDelegate (callfun);
这样应该是方向^_^。


恩,HPP,我不怎么喜欢这个后缀,:-),不错。

#17楼  回复 引用 查看   

2007-10-16 10:34 by 随风逝去      
先mark

#18楼  回复 引用 查看   

2008-03-11 10:38 by lbq1221119      
我提个问题啊 想了挺久没有想明白的
两个对象arraylist,如何使用委托实现之间的赋值?来达到效率最高?

#19楼  回复 引用   

2008-09-17 18:39 by liqy17[未注册用户]
好文章!这在Windows编程中,确实是个很不错的改善实现方法。不然很多消息机制非常依赖于操作系统,移植性很差。

#20楼  回复 引用 查看   

2011-01-16 23:39 by MOMO爱Toda      
不就是函数指针吗?

#21楼  回复 引用 查看   

2011-01-16 23:44 by MOMO爱Toda      
我觉得委托和面向对象的思想相对独立