原生(Delphi)回调在 .NET (C#) COM 程序集中
如何将原生(Delphi)回调指针传递给 .NET COM 程序集
引言
我们假设以下情况
- 您想编写一个 .NET 方法,并使其可供原生编程语言使用。
- 此 .NET 方法将回调作为参数。
- 您想从 Delphi 应用程序调用此 .NET 方法。
背景
首先,什么是回调?根据维基百科,回调是对一段可执行代码的引用,它作为参数传递给另一段代码。这允许较低级别的软件层调用定义在高层软件层中的子程序(或函数)。在原生环境中处理回调相当直接。当然,在 .NET 中,我们有代表(delegates),它们最接近这个概念,但我们如何在原生和 .NET 之间使用/传递回调呢?这正是本文主要探讨的内容。上述场景中的另一个问题是使 .NET 方法对原生世界可见。这可以通过使 .NET 程序集 COM 可见来实现,以下是使 .NET 程序集 COM 可见的几个重要步骤。
- 将程序集注册为 COM 对象。
- 将程序集放入全局程序集缓存 (GAC) 中,如果您想从不同的路径/位置调用它。
- 为程序集提供强名称,如果您想将其添加到 GAC 中。
.NET 部分
我将使用 Visual Studio 2010 和 C# 来构建这部分。在 VS2010 中,添加一个新的类库项目并命名为 CSDemoLibrary。在项目属性和程序集信息对话框中,勾选“使此库 COM 可见”。
如果我们想将此程序集添加到全局程序集缓存 (GAC) 中,我们需要为它提供一个强名称。要提供此名称:转到“签名”选项卡,勾选“签名程序集”,然后从下拉列表中选择“<新建…>”,并将文件命名为 CSDemoLibrary。
因为我们需要将指针传递到此程序集,所以还需要一个额外的步骤,即在此程序集中启用不安全代码,方法是在“生成”选项卡中勾选“允许不安全代码”。
现在我们可以编写代码了。删除项目中默认的类,并添加新类,将其命名为 MyClass
。
为了简单起见,在此演示中,我们将使用一个简单的回调,它只接受两个参数(整数和字符串)。在 Delphi 中,此回调函数将声明如下:
procedure callback(intParam: Integer; strParam: pChar);
因此,要在 .NET 中使用此回调的等价物,请声明一个代表(delegate)如下:
public delegate void NativeCallback(Int32 intParam, [MarshalAs(UnmanagedType.LPWStr)] string strParam);
我在这里使用了 [MarshalAs(UnmanagedType.LPWStr)]
属性,以便将字符串转换为 Unicode/宽字符串。如果您只想传递 ANSI 字符串,可以省略此属性(在这种情况下,Delphi 方法中的 strParam
应该为 pAnsiChar
)。现在,让我们编写 .NET 方法。首先,我们需要使用 ComVisible(true)
属性和一个唯一的 GUID 字符串使 MyClass
COM 可见,并且因为此 .NET 方法将处理原生指针(回调指针),所以我们必须使用 (unsafe
) 关键字标记它。以下是 MyClass.cs 的完整代码:
using System;
using System.Runtime.InteropServices;
using System.Threading;namespace CSDemoLibrary
{
public delegate void NativeCallback(Int32 intParam,
[MarshalAs(UnmanagedType.LPWStr)] string strParam);
[ComVisible(true),
GuidAttribute("3A65D04A-3F2F-4CB3-B65A-8D402B8C64CE")]
public class MyClass
{
public unsafe int Process(
int intValue,
Int32 callbackPointer,
int intParam,
string strParam)
{
IntPtr ptr = new IntPtr(callbackPointer);
NativeCallback callbackMethod =
(NativeCallback)Marshal.GetDelegateForFunctionPointer(ptr, typeof(NativeCallback));
callbackMethod(intParam, strParam);
Thread.Sleep(1000);
callbackMethod(25, "first step");
Thread.Sleep(1000);
callbackMethod(50, "second step");
Thread.Sleep(1000);
callbackMethod(75, "third step");
Thread.Sleep(1000);
callbackMethod(100, "fourth step");
return intValue * 10;
}
}
}
在前面的代码中,Process
方法模拟了一个耗时的进程(例如从互联网获取大量数据),并通过从原生主机应用程序进行回调来发送状态通知。在我们的演示中,为了简单起见,我们使用 Thread.Sleep
将执行暂停一秒钟,以模拟一个返回整数值的耗时进程。在此方法中,callbackPointer
是原生回调指针,(intParam
, strParam
) 是回调参数。这两个参数将用作回调的初始化参数。在许多情况下,不需要这些初始化参数,但我在这里保留它们是为了展示如何将它们从原生应用程序传递给 .NET 方法。GetDelegateForFunctionPointer
方法是此解决方案的关键;它将非托管函数指针转换为代表。有关此方法的更多信息,请 在此 处查看。
注册 .NET 程序集
为避免路径问题,我将使用(Visual Studio 命令提示符)。在(开始菜单 -> 所有程序 -> Microsoft Visual Studio 2010 -> Visual Studio Tools)中对此工具有快捷方式。使用此工具,您可以按照以下步骤注册 COM 程序集:
- 以管理员身份运行 Visual Studio 命令提示符,然后使用常规的 DOS 命令导航到 CSDemoLibrary.dll 文件夹。
- 如果您打算将 .NET 程序集 CSDemoLibrary.dll 放在原生应用程序(.exe)的同一目录下,您可以忽略将程序集添加到全局程序集缓存 (GAC) 的步骤,否则您应该将其添加到 GAC。要做到这一点,请使用带 –I 参数的 gacutil,如下所示:gacutil -i CSDemoLibrary.dll。
- 使用 regasm 注册 COM 程序集,如下所示:regasm CSDemoLibrary.dll。
注释
- 要从 GAC 中移除 .NET 程序集:gacutil -u CSDemoLibrary。
- 要取消注册 .NET 程序集:regasm -u CSDemoLibrary.dll。
原生(Delphi)部分
我将为此部分使用 Delphi 2010。在 Delphi 2010 IDE 中,创建一个新的 VCL 应用程序。将(TLabel
、TButton
、TGauge
)添加到主窗体。
在主窗体的代码后端,将 ComObj
添加到 uses 部分。创建一个简单的回调过程来更新 Gauge1
的进度和 lbMessage
的标题,如下所示:
procedure callback(intParam: Integer; strParam: pChar); stdcall;
Begin
Form2.Gauge1.Progress := intParam;
Form2.lbMessage.Caption := strParam;
Application.ProcessMessages;
End;
我将在本演示中使用后期绑定,因此请更新 btnProcess
的点击事件处理程序,如下所示:
procedure TForm2.btnProcessClick(Sender: TObject);
var
oleObject: OleVariant;
begin
try
oleObject := CreateOleObject('CSDemoLibrary.MyClass');
ShowMessage('result= ' + IntToStr(oleObject.Process(10, LongInt(@callback), 0, 'initialization')));
except on E: Exception do
ShowMessage('COM Error: ' + #13 + #10 + e.Message);
end;
end;
此应用程序调用 .NET 耗时进程,该进程通过回调过程将进程状态通知原生应用程序,最后显示 .NET 方法/进程的结果。
参考文献
- http://en.wikipedia.org/wiki/Callback_%28computer_programming%29
- http://edn.embarcadero.com/article/32754
- http://msdn.microsoft.com/en-us/library/system.runtime.interopservices.marshal.getdelegateforfunctionpointer%28v=vs.85%29.aspx
关注点
我希望本文展示了在 Delphi 中使用 C# COM 程序集是多么容易。