使用 IJW 实现回调函数(避免 DllImport)






4.53/5 (14投票s)
2002年7月14日
4分钟阅读

211769

1015
演示了如何使用 IJW 调用需要回调的本机 API 函数,而无需使用 DllImport 属性。该技术允许您像 Microsoft 推荐的那样传递一个委托作为回调函数,不同之处在于,我展示了如何在没有难看的 DllImport 属性的情况下实现这一点。
引言
这一切都始于有一天,在 Microsoft 的 dotnet.languages.vc 新闻组中,有人抱怨他难以从托管 C++ 中使用 EnumWindows
。他非常坚定地表示,他不想使用 DllImport
属性。这自然引起了我的兴趣,我想我或许可以帮助他。令我非常失望的是,我发现自己也遇到了困难。问题在于 EnumWindows
的第一个参数是一个回调函数。我在 MSDN 和 Google 上搜索的所有解决方案都显示了如何使用 DllImport
属性来实现这一点。建议的技术很简单。我们要声明一个与回调函数完全相同的 __delegate
对象。然后,我们要使用 DllImport
来定义 EnumWindows
,使其第一个参数接受我们的 __delegate
类型。现在,我们可以简单地将回调函数写成托管类的一个成员,并将这个函数传递给 EnumWindows
。
//declare our delegate
__delegate bool CallBack(IntPtr hwnd, IntPtr lParam);
...
//ugghhhhhhh!!!! so uglyyyyyy!!!
[DllImport("user32")]
extern "C" int EnumWindows(CallBack* x, int y);
...
//create the delegate
CallBack* cb = new CallBack(0,
&SomeClass::SomeMatchingMethod);
//call the function
EnumWindows(cb, 0);
问题所在
这一切都很好,但开始变得令人讨厌。我的问题是,无论我怎么做,都无法让回调函数正常工作。显然,我不能直接传递一个委托,因为当我们使用 IJW 时,本机 API 函数期望的是本机参数,而不是托管参数。我甚至尝试了一些愚蠢的事情,比如将一个委托对象转换为 WNDENUMPROC
,正如你可能猜到的,我彻底失败了。我还尝试传递托管类的静态和实例成员作为回调函数,但不断收到关于 NULL
引用和对象的运行时异常。这至少可以说令人非常失望。
就在这时,我得到了 Richard Grimes 的巨大帮助,他是一位 Microsoft MVP,撰写了多本关于 Microsoft 编程技术的优质书籍。他的最新著作是关于使用托管扩展进行 VC++ .NET 编程的。在我关于使用 IJW 调用 EnumWindows
的查询回复中,他回复了我,回复中包含了他最新著作中的一个示例代码片段,但不幸的是,他使用了 DllImport
。我回复说我不是在寻找 DllImport
,而且我必须说,我的沮丧一定在我的回复中反映得不好。因为 Richard 的回答一开始也有点生硬。但他给了我第一个线索,告诉我为什么我走错了方向。他向我解释了托管类成员如何使用 __clrcall
调用约定,以及非托管回调函数如何使用 __stdcall
调用约定。事实上,当我仔细查看编译器警告时,我震惊地发现一条消息说我正试图尝试重新定义调用约定,从 __clrcall
到 __stdcall
,这是不可能的,因此被忽略了。这时我才意识到,我必须放弃尝试使用托管类成员方法作为我的回调。
解决方案
Richard 的最终答案是坚决的“否”。但我非常想找出一种方法,让托管类能够将委托作为回调函数传递。这时,一个想法突然闪现。内部类。我们可以使用内部类!我们所要做的就是有一个 __gc
类,其中包含一个内部 __nogc
类,而外部托管类将封装内部非托管类并将其暴露给外部世界。外部类有一个委托,作为托管回调。内部 __nogc
类有一个原生的 __stdcall
方法作为回调函数。每次调用时,此回调函数将调用托管委托。因此,我们在这里模拟了一个托管回调机制。我在关键区域添加了注释,以便您能更好地理解。
__gc class CEnumWindows //outer class
{
private:
__nogc class _CEnumWindows //inner class
{
private:
/* This is a native function that follows the */
/* __stdcall calling convention that's required */
static BOOL CALLBACK EnumWindowsProc(HWND hwnd, LPARAM)
{
// We need to get the managed callback
// up for each instance that our callback
// gets called. So we get a pointer to
// the current instance of the outer class
// and invoke the delegate that is holding
// the managed callback method that the
// callee code has passed to us
CEnumWindows* pew = CEnumWindows::GetClass();
pew->m_EnumProc->Invoke(hwnd, NULL);
return TRUE;
}
public:
void StartFinding()
{
EnumWindows((WNDENUMPROC)_CEnumWindows::EnumWindowsProc,NULL);
}
};
private:
_CEnumWindows* m_ew;
public:
__delegate bool EnumProc(IntPtr hwnd, IntPtr lParam);
static CEnumWindows* GetClass()
{
//This for the unmanaged class to use
//when it needs a pointer to the managed class
return m_pclass;
}
static CEnumWindows* m_pclass=NULL;
CEnumWindows()
{
m_pclass = this;
m_ew = new _CEnumWindows(); //unmanaged heap
}
~CEnumWindows()
{
// we need to delete the object manually
// as is is on the unmanaged heap
delete m_ew;
}
void StartFinding()
{
m_ew->StartFinding();
}
EnumProc* m_EnumProc;
};
现在,我们可以从任何托管类中使用它,并将任何托管类成员函数作为回调函数传递。在下面的示例中,我创建了 CEnumWindows
的新实例,它是外部类。然后,我将一个来自我的类的托管函数关联到 CEnumWindows
对象的委托成员。好吧,好吧,我知道使用公共委托成员不是一个 proper 的方式,但我只是想演示一下如何做到这一点。如果您愿意,可以将其放入属性中,或者编写一个函数来为您完成此操作。
CEnumWindows* p = new CEnumWindows();
p->m_EnumProc = new CEnumWindows::EnumProc(this,&NForm::EWHandler);
p->StartFinding();
结论
出于我自己的奇思妙想,我是使用 IJW 的忠实粉丝,我觉得它比使用那些让你的代码看起来像 C# 或 VB .NET 的奇怪属性更自然。我并不是反对其他语言,但我更喜欢我的 C++ 代码看起来像 C++,而不是某种丑陋的、主观上劣等的语言的变种。总之,感谢 Richard Grimes 指明了正确的方向。那些对他的新书《使用托管扩展进行 Microsoft® Visual C++® .NET 编程》感兴趣的人可以访问这个链接。 Programming with Managed Extensions for Microsoft® Visual C++® .NET (Microsoft Press)