65.9K
CodeProject 正在变化。 阅读更多。
Home

函数指针和委托 - 缩小差距!

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.55/5 (21投票s)

2005年6月17日

Ms-PL

4分钟阅读

viewsIcon

170751

解释了 Marshal 类方法 GetFunctionPointerForDelegate 和 GetDelegateForFunctionPointer 的用法,并比较了它们的性能与 P/Invoke 机制。

引言

3 年前,我写了一篇题为 使用 IJW 实现回调函数(避免 DllImport) 的文章,其中介绍了一种在不使用 DllImport 机制的情况下调用需要回调的 API 函数的技术。我使用的技术涉及声明一个 __gc 类,其中包含一个内部的 __nogc 类,该 __gc 类包装了内部类,并向外部世界公开了一个托管接口。虽然这是一种相当曲折的技术,但在当时是唯一的选择。但今天,随着 Whidbey 的发布临近,情况已经大有改善,Marshal 类引入了两个新方法 - GetFunctionPointerForDelegateGetDelegateForFunctionPointer。现在你可以获取一个函数指针并在其周围包装一个委托,或者反过来 - 获取一个委托并将其用作函数指针。本文使用几乎是传奇的 EnumWindows 示例来演示这两种技术,该示例是每个人在想演示与回调函数相关的内容时都会使用的。

使用示例

delegate BOOL EnumWindowsDelegateProc(HWND hwnd,LPARAM lParam);

我声明了将要暴露给 .NET 世界的委托类型(至少是 C++ .NET 世界,因为我使用了一些本机类型)。委托类型匹配 EnumWindows 函数的回调原型。

ref class WindowEnumerator
{
private:
    EnumWindowsDelegateProc^ _WindowFound;
public:
    WindowEnumerator()
    {
        _WindowFound = nullptr;
    }
    event EnumWindowsDelegateProc^ WindowFound
    {
    public:
        void add(EnumWindowsDelegateProc^ d)
        {
            if(_WindowFound == nullptr)
                _WindowFound = d;                
        }
        void remove(EnumWindowsDelegateProc^)
        {            
            _WindowFound = nullptr;
        }
    }
    void Init()
    {
        pin_ptr<EnumWindowsDelegateProc^> tmp =  &_WindowFound;
        EnumWindows((WNDENUMPROC)Marshal::GetFunctionPointerForDelegate(
            _WindowFound).ToPointer(), 0);        
    }
};

我有一个非简单的事件(WindowFound),它的类型是前面声明的委托类型 - 之所以必须使用非简单事件,是因为 C++/CLI 不将事件视为常规类成员,并且不允许对事件成员执行诸如 &(地址)之类的操作。注意我是如何在赋值事件处理程序之前检查 nullptr 的 - 这是为了确保只有一个委托与该事件相关联。代码的关键在于 Init 方法,我在其中直接调用了 EnumWindows,并使用 GetFunctionPointerForDelegate 将委托对象转换为函数指针。如果你使用 Reflector 或 ILDasm 查看 *mscorlib* 源代码,你会看到 GetFunctionPointerForDelegate 会进行 nullptr 检查,然后调用一个 extern 函数 GetFunctionPointerForDelegateInternalGetFunctionPointerForDelegateInternal 会围绕传入的委托创建一个本机可调用的存根,我最好的猜测是它定义在 *mscorwks.dll* 中(这只是一个猜测,CLR 团队中的人会更清楚)。请注意,我使用一个临时的 pin_ptr 变量来固定委托对象,因为在原生代码中使用函数指针时,我不希望委托对象在 CLR 堆上被移动。

delegate int DispProc(String^, String^);

在这里,我声明了我的第二个委托(我将用它来包装 CRT 库提供的 printf 函数)。

ref class MyClass
{
public:
    static BOOL HandleFoundWindow(HWND hwnd,LPARAM lParam)
    {
        char buff[512];
        GetWindowText(hwnd, buff, 511);
        if(IsWindowVisible(hwnd) && pDispProc && strlen(buff))
            pDispProc("%s\r\n",gcnew String(buff));
        return TRUE;
    }
    static DispProc^ pDispProc =  nullptr;
};

这个类只是定义了一个 staticHandleFoundWindow 方法,该方法匹配 WindowEnumerator 类期望的回调原型。请注意,我本可以使用 Console::WriteLine,但我希望使用一个包装了函数指针的委托(这样我就可以演示 GetDelegateForFunctionPointer 方法的使用。写了这么多文章和一本技术书籍后,我在发明不寻常的曲折例子来演示编码技术时已经丧失了任何羞耻感。许多像我一样从事技术写作的人都以创造最美的代码而闻名,而这些代码在现实世界的生产环境中却毫无用处。所以,请大家多多包涵。

int main(array<System::String ^> ^args)
{
    WindowEnumerator wenum;
    EnumWindowsDelegateProc^ d1 = gcnew EnumWindowsDelegateProc(
        MyClass::HandleFoundWindow);
        
    wenum.WindowFound += d1;        
    
    HMODULE h1 = LoadLibrary("msvcrt.dll");
    if(h1)
    {
        typedef int (*FUNC_PTR)(const char *, ...); 
        FUNC_PTR pfn = reinterpret_cast<FUNC_PTR>(GetProcAddress(h1, "printf"));
        if(pfn)
        {
            DispProc^ d2 = (DispProc^)Marshal::GetDelegateForFunctionPointer(
                (IntPtr)pfn,DispProc::typeid);
            MyClass::pDispProc = d2;
            wenum.Init();
        }
        
        FreeLibrary(h1);
    }
    
    return 0;
}

我使用 LoadLibrary/GetProcAddress 获取 printf 函数的指针,并调用 GetDelegateForFunctionPointer 来创建一个包装此函数指针的委托。如果你查看 GetDelegateForFunctionPointer 的实现,你会发现它进行了一些 nullptr 检查,然后检查你是否确实传递了一个 Delegate 类型,然后调用一个 extern 函数 GetDelegateForFunctionPointerInternalGetDelegateForFunctionPointerInternal 会创建一个包装本机函数指针的新委托(类似于 P/Invoke 机制),它定义在一个未文档化的 DLL 中 - 正如我之前提到的,我最好的猜测是 *mscorwks.dll*,但我可能错了。MSDN 说传入的函数指针必须是纯粹的非托管函数指针 - 这意味着你不能在混合模式模块中安全地传递本机指针。尽管我在混合模式项目中测试过本地函数,但效果还算顺利 - 但遵循 MSDN 指南更安全 - 否则你永远不知道什么时候会出问题。

性能改进

我做了一个小速度测试,看看哪个性能更好。我写了两个类 - 都使用 EnumWindows API 枚举窗口,其中一个使用 P/Invoke,另一个使用 Marshal::GetFunctionPointerForDelegate 方法手动进行委托到函数指针的转换。正如我所料,后一种方法更有效率,并且随着迭代次数的增加,这种效率的提高也相当稳定。

这是两个类:

delegate int DEnumWindowsProc(IntPtr hWnd, IntPtr lParam);

ref class Base abstract
{
public:
    static int EnumWindowsProc(IntPtr hWnd, IntPtr lParam)
    {
        return TRUE;
    }
    virtual void Init() abstract;
};

ref class PIClass : Base
{
public:
    [DllImport("user32.dll")]
    static bool EnumWindows(DEnumWindowsProc^ lpEnumFunc, 
        IntPtr lParam);

    virtual void Init() override
    {
        EnumWindows(gcnew DEnumWindowsProc(EnumWindowsProc), 
            IntPtr::Zero);        
    }

};

ref class FuncPtrClass : Base
{
public:
    static DEnumWindowsProc^ dg = gcnew DEnumWindowsProc(EnumWindowsProc);

    virtual void Init() override
    {
        pin_ptr<DEnumWindowsProc^> tmp =  &dg;
        ::EnumWindows((WNDENUMPROC)
            Marshal::GetFunctionPointerForDelegate(dg).ToPointer(), 0);            
    }
};

这是我的测试代码:

generic<typename T> where T : Base int CalcSpeed(int count)
{    
    Console::WriteLine(
        "Checking class : {0} with {1} iterations", T::typeid, count);
    T t = Activator::CreateInstance<T>();        
    DWORD start = GetTickCount();
    while(count--)
        t->Init();
    DWORD stop = GetTickCount();
    Console::WriteLine("{0} milliseconds", stop - start);
    return stop - start;
}

void DoCalc(int count)
{
    int t1 = CalcSpeed<PIClass^>(count);
    int t2 = CalcSpeed<FuncPtrClass^>(count);
    float pc = (float)(t1-t2)/t1 * 100;
    int pcrounded = (int)pc;
    Console::WriteLine(
        "{0}% improvement for {1} iterations\r\n", pc, count);
}

int main(array<System::String ^> ^args)
{    
    DoCalc(10000);
    DoCalc(50000);
    DoCalc(100000);
    DoCalc(500000);
    return 0;
}

这是我得到的输出(你们会得到略有不同的结果,这是很正常的):

历史

  • 2005 年 6 月 17 日:文章首次发布。
© . All rights reserved.