用 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/14103973/interface-result-delphi-to-c-sharp-class-define-dll-use-in-c-sharp

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

posted @ 2017-01-23 12:37  海利鸟  阅读(93)  评论(0编辑  收藏  举报