用 Lazarus 开发 OPC Client 4 (DLL 与 Dot Net 互通)
由于项目历史原因所开发的功能需要提供给.NET环境下应用使用。这个应该算是难点,C#、dot NET 是托管代码,资源的释放,是GC完成的。简单数据类型可以按对应声明,复杂的数据就更麻烦了。
这里要再他谈字符集。
windows NT及之后系统内部用UNICODE作为默认字符集,这也就是看到很多Windows API提供了 Ansi版本的函数和Wide Char版本的函数,函数名称一般用A或WC以示区别。比如:strcpy函数以及它的宽字符版本wcscpy。
.NET C# 和 Delphi\Lazarus都属于强类型语言,在字符集处理上有先天的有时,尤其是Delphi较Lazarus更为方便,Delphi到目前版本都是紧跟Windows核心,虽然它目前也可以生成IOS和Andoid程序,据说效果还不错。Lazarus在实现上较为麻烦,从1.4版本以后Lazarus随FPC升级,内核从Ansi默认为UTF8,这导致好多Delphi程序不可以平滑移植,Lazarus与.Net 的就更为麻烦了,为了减少壁垒,当然希望用的范围更广,我计划将采用Ansi作为默认方式。
前一篇博文提到用接口,这也是为了软件互通做的安排。
要把一个类从一个语言暴露到另一个语言并正常使用,这个基本上是不可能的,Delphi很早为了与C/C++的程序互动也就是通过头文件函数声明使用互相使用,Delphi对如何使用类也给出了相应的解决方案(接口),所以Lazarus也是通过相同的方式。
Pascal 声明及部分实现
unit OpcGeneralKitDll; {$mode objfpc}{$H+} . . .
interface procedure BuildOpcClient(out IClient: IOpcClient;const HostName: PAnsiChar = ''; const ProgID: PAnsiChar = ''); stdcall; export;
exports BuildOpcClient; implementation
. . .
procedure BuildOpcClient(out IClient: IOpcClient;const HostName: PAnsiChar = ''; const ProgID: PAnsiChar = ''); stdcall; export; begin IClient := TOpcSimpleClient.Create(HostName, ProgID); end; end.
.Net C# 声明
[DllImport("OpcGeneralKitDLL.dll", EntryPoint = "BuildOpcClient", CharSet = CharSet.Ansi, CallingConvention = CallingConvention.StdCall)] public static extern void BuildOpcClient([MarshalAs(UnmanagedType.Interface)] out IOpcClient IClient, [MarshalAs(UnmanagedType.LPStr)] string aHostName = "", [MarshalAs(UnmanagedType.LPStr)] string aProgID = "");
这是部分很关键,通过IOpcClient 接口返回了OPC类库的主要操作。后附上部分IOpcClient接口C# 与 Pascal 接口对照。
.Net中使用System.Runtime.InteropServices空间的UnmanagedType指定如何将参数或字段封送到非托管代码。
这里关键有对String使用,字符串属于复杂数据,而且实现形式也很多,有计数形式的、有长度指示的、有NULL\nil指示结尾的,不过幸好.Net C#对于这些都支持(参见unmanagedtype)。
测试了[MarshalAs(UnmanagedType.LPStr)] ,[MarshalAs(UnmanagedType.BStr)],[MarshalAs(UnmanagedType.BStr)]作为输入参数都可以正常使用,但作为字符串输出就比较麻烦了,只能使用[MarshalAs(UnmanagedType.BStr)]当作WideString也就是UNICODE字符串来处理,或者非要AnsiString的话只能传送指针,然后通过 Marshal.PtrToStringAnsi(ptr);
不知道为什么获取函数结果返回与输出到最后可能效果不一样,就算结果一样,但由于GC可能之后发生变化了即可能被GC给释放掉,所以之后都调整为函数输出字符串,包括方法和其它,而非返回字符串,虽然.Net 的官方方案建议通过指针然后使用变换函数如下声明:
//C#声明 [MethodImplAttribute(MethodImplOptions.PreserveSig)] IntPtr GetMethodValueAsString(); //C#调用的时候: IntPtr ptr = instance.GetMethodValueAsString(); string result = Marshal.PtrToStringAnsi(ptr);
比较麻烦吧!之后改为如下输出方式,比较简单的方式就是当成BSTR/WideString来处理
Pascal 声明
procedure GetMethodValueAsString(out value: WideString); stdcall;
C#对应声明
[MethodImplAttribute(MethodImplOptions.PreserveSig)] void GetMethodValueAsString([MarshalAs(UnmanagedType.BStr)] out string result);
简单多了吧!
Pascal –> .Net C# 声明
IOpcClient = interface ['{93CFA635-E7EC-49B8-87E6-4BACF701BF7B}'] procedure Connect();stdcall; procedure Disconnect;stdcall; function ServerState: LongInt;stdcall; function GetCount: LongInt;stdcall; procedure GetGroup(const Index: LongInt;out Result:IOpcSimpleGroup);stdcall; procedure SetProgID(const aValue: PAnsiChar);stdcall; procedure GetOnConnect(out Result: TNotifyEvent);stdcall; procedure SetOnConnect(const aValue: TNotifyEvent);stdcall; function Add(const aGroupName: PAnsiChar; const aUpdateRate: LongInt=100; const aEnbled :LongBool =true): LongInt; overload;stdcall; end;
.Net C# –> Pascal 声明
[ComVisible(true)] [ComImport, Guid("93CFA635-E7EC-49B8-87E6-4BACF701BF7B"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] public interface IOpcClient { [MethodImplAttribute(MethodImplOptions.PreserveSig)] void Connect(); [MethodImplAttribute(MethodImplOptions.PreserveSig)] void Disconnect(); [MethodImplAttribute(MethodImplOptions.PreserveSig)] int GetCount(); [MethodImplAttribute(MethodImplOptions.PreserveSig)] void GetGroup(int Index, [MarshalAs(UnmanagedType.Interface)] out IOpcGroup refG); [MethodImplAttribute(MethodImplOptions.PreserveSig)] void GetProgID([MarshalAs(UnmanagedType.BStr)]out string Result); [MethodImplAttribute(MethodImplOptions.PreserveSig)] void SetProgID([MarshalAs(UnmanagedType.LPStr)] string aValue); [MethodImplAttribute(MethodImplOptions.PreserveSig)] void GetOnConnect([MarshalAs(UnmanagedType.FunctionPtr)] out TInfoEvent refFuc); [MethodImplAttribute(MethodImplOptions.PreserveSig)] void SetOnConnect([MarshalAs(UnmanagedType.FunctionPtr)] TInfoEvent aValue); [MethodImplAttribute(MethodImplOptions.PreserveSig)] int Add([MarshalAs(UnmanagedType.LPStr)] string aGroupPathName, int aUpdateRate = 100, bool aEnbled = true); }
再次强调,C#如果不能明确类型,.NET必须用“特性”将非托管类型指定,所以最好用明确的数据类型
Pascal | 字节数 | .net | 使用时特性修饰 |
Boolean | 1 | bool | [MarshalAs(UnmanagedType.U1)] bool |
LongBool | 4 | bool | 无需修饰 或者 [MarshalAs(UnmanagedType.Bool)] bool |
integer | 2,4 | int | 无需修饰 或者 [MarshalAs(UnmanagedType.SysInt)] int |
longInt | 4 | Int32,long | 无需修饰 或者 [MarshalAs(UnmanagedType.I4)] int |
LongWord | 4 | Uint32,ulong | 无需修饰 或者 [MarshalAs(UnmanagedType.U4)] uint |
TInfoEvent 回调函数 | 4 | 定义委托 TInfoEvent | [MarshalAs(UnmanagedType.FunctionPtr)] TInfoEvent |
以此类推 . . . . |
最后要谈谈 Native DLL 与 .NET (原生DLL与Dot Net),对于回调函数的注意事项。
主要是两点,一个是正常调用,二个就是关于正常使用。
一个是正常调用
dot NET 本身最原生态有太多的支持方式,导致可以先转化为指针应用然后再在 dot NET 里面使用,其实用UnmanagedType.FunctionPtr修饰以后就可以直接使用指针函数了,也就是dot NET里面的委托。但发现一会就不能用了。
都是GC惹的祸,被它释放了!
二个就是关于正常使用
正常使用就是避免函数引用被释放,一开始和很多朋友想的办法一样——用静态,但是没有成功(可能规模不大有成功的),然后就是看到一大神接的方法让委托在GC类保持,通过GC.KeepAlive,之后一切正常了。
简体中文版报错信息:
xxxx::Invoke”类型的已垃圾回收委托进行了回调。这可能会导致应用程序崩溃、损坏和数据丢失。向非托管代码传递委托时,托管应用程序必须让这些委托保持活动状态,直到确信不会再次调用它们
if (OnConnectDelegate == null) { OnConnectDelegate = new TInfoEvent(OnConnect); GC.KeepAlive(OnConnectDelegate); }
使用以上方法,保持委托活动状态,运作正常。
参考网文:
http://stackoverflow.com/questions/30041480/call-delphi-function-from-c-sharp
http://blog.csdn.net/cmd9x/article/details/51507193
http://blog.csdn.net/catshitone/article/details/53641498
https://msdn.microsoft.com/zh-cn/library/system.runtime.interopservices.unmanagedtype(v=vs.110).aspx
https://msdn.microsoft.com/zh-cn/library/zah6xy75
https://msdn.microsoft.com/en-us/library/zah6xy75.aspx?cs-save-lang=1&cs-lang=csharp