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

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

starIconstarIconstarIconstarIconstarIcon

5.00/5 (29投票s)

2005年9月13日

3分钟阅读

viewsIcon

336491

downloadIcon

4022

提供了一种在 C# 和 VB 中使用 C 调用约定回调函数的简便方法

问题

我们假设以下情况成立

  1. 一个非托管 DLL 中的函数,它接受函数指针作为参数。
  2. 函数指针的类型遵循 C 调用约定(声明为 __cdecl)。
  3. 您需要在 C# 或 VB 程序中导入该函数,并向其传递一个委托。
  4. 您的委托将被调用不止一次。

这是一个例子。

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++ 非托管库。

© . All rights reserved.