在 C# 和 VB 中使用 C 调用约定回调函数 - 简便方法





5.00/5 (29投票s)
2005年9月13日
3分钟阅读

336491

4022
提供了一种在 C# 和 VB 中使用 C 调用约定回调函数的简便方法
问题
我们假设以下情况成立
- 一个非托管 DLL 中的函数,它接受函数指针作为参数。
- 函数指针的类型遵循 C 调用约定(声明为
__cdecl
)。 - 您需要在 C# 或 VB 程序中导入该函数,并向其传递一个委托。
- 您的委托将被调用不止一次。
这是一个例子。
DLL 库头文件
typedef void (__cdecl *func_type)(int count);
TESTLIB_API void __cdecl SetCallback( func_type func );
C# 代码
[DllImport("TestLib.dll",CallingConvention=CallingConvention.Cdecl)]
public static extern void SetCallback( MulticastDelegate callback );
您会定义一个委托类型
public delegate void CallbackDelegate( int count );
并将其实例传递给非托管函数。
[STAThread]
static void Main(string[] args)
{
CallbackDelegate del = new CallbackDelegate( Callback );
SetCallback( del );
}
private static void Callback( int count )
{
Console.WriteLine( "Callback invoked for " +
count + " time" );
}
然后,您的委托应该被调用一次或多次。
问题在于,非托管函数接受的函数指针应该遵循 C 调用约定,但您传递给它的委托实例却没有(它遵循标准的调用约定(__stdcall
)。结果是,从非托管代码调用该方法后,堆栈会损坏,在第二次或第三次调用时,会抛出 System.NullReferenceException
。
可以在 这里 找到对同一问题的描述。请注意,该问题仅存在于接受参数的回调函数中。
解决方案
解决方案可以在 这里 和 这里 找到。解决方案是(如以上帖子所示)在委托类型的 "Invoke
" 方法上应用修改选项。
.method public hidebysig virtual instance native int
modopt([mscorlib]System.Runtime.CompilerServices.CallConvCdecl)
Invoke(int32 cb) runtime managed
{
} // end of method ...::Invoke
C# 和 VB 编译器生成的原始方法如下所示
.method public hidebysig virtual instance native int
Invoke(int32 cb) runtime managed
{
} // end of method ...::Invoke
(无修改选项)
这不能直接在您的代码中完成,因为在 C# 和 VB 中没有办法指定修改选项(至少是与调用约定相关的修改选项)。
该解决方案的缺点是,您需要反汇编您的程序集,添加修改选项,然后进行编译。这也不是什么大问题,但如果可以在运行时应用解决方案,而无需反汇编和重新编译代码呢!
运行时委托类型生成
本文提出的解决方案正是通过生成委托类型来实现这一点,但它是在运行时完成的:它允许您为任何方法生成和使用委托类型。
更具体地说,它
- 能够为任何方法和指定的
System.Runtime.InteropServices.CallingConvention
生成委托类型。 - 生成的类型与 C# 和 VB 编译器生成的类型完全相同,但在 "
Invoke
" 方法上应用了修改选项。 - 能够创建生成类型的实例。
- 为每个方法和
CallingConvention
缓存生成的类型。
然而,有些功能尚未实现:C# 或 VB 编译器生成的委托可以为参数提供封送信息。
如果在委托声明的参数上应用 [MarshalAs]
属性,则会为 C# 或 VB 编译器生成的委托类型中的这些参数指定封送信息。此解决方案不提供指定参数封送的方法,尽管它可以实现。其他任何参数属性,如 [In]
、[Out]
、ref、默认值等,都会被处理(请参阅 System.Reflection.ParameterAttributes
)。
使用解决方案
要获得委托实例,只需调用 App.Runtime.InteropServices.DelegateGenerator
的方法之一。该方法将生成一个委托类型(如果尚未为特定方法和调用约定生成),并返回一个实例。
为了使委托生成对您的代码透明,您可以使用如下代码:
/// <summary>
/// Callback delegate.
/// </summary>
public delegate void CallbackDelegate( int count );
/// <summary>
/// Sets specified callback method.
/// </summary>
/// <param name="callback"></param>
public static void SetCallback( CallbackDelegate callback )
{
SetCallback( DelegateGenerator.CreateDelegate( callback ) );
}
/// <summary>
/// Sets specified callback method.
/// </summary>
/// <param name="callback"></param>
[DllImport("TestLib.dll",
CallingConvention=CallingConvention.Cdecl)]
private static extern void SetCallback(
MulticastDelegate callback );
您公开一个委托类型和一个接受该委托类型实例的 "代理" 方法,并将该实例的调整版本传递给导入的方法。
实现
委托类型是通过使用 System.Reflection.Emit
命名空间中声明的类型生成的。不幸的是,它们不提供指定修改选项的方法,因此修改选项是通过对生成方法的签名进行一些 "黑客" 处理来实现的。这涉及到使用反射。
源代码和演示
源代码包括 App.Runtime.InteropServices.DelegateGenerator
类型和一个反射助手类型。
有两个用 C# 和 VB 实现的演示项目,它们演示了生成委托和普通委托的用法。使用普通委托会导致堆栈损坏和 System.NullReferenceException
。
演示包含 C++ 非托管库。