• C#与C++交互的一些基础


    好久没写博客了,因为最近很忙,所以需要一些时间来整理下自己遇到的问题

    最近在搞C#调用C++封装的DLL

    由于是托管代码调用非托管代码,所以期间遇到了很多问题,也很扯淡

    C#引用C++的API,无法像传统的方式一样,使用右键->引用来完成对程序集的添加。因此我们需要使用到System.Runtime.InteropServices中的DllImport特性,下面我们来了解下它。

     

    DllImportAttribute的定义如下:

    // 摘要:
        //     指示该特性化方法由非托管动态链接库 (DLL) 作为静态入口点公开。
        [AttributeUsage(AttributeTargets.Method, Inherited = false)]
        [ComVisible(true)]
        public sealed class DllImportAttribute : Attribute
        {
            // 摘要:
            //     将 Unicode 字符转换为 ANSI 字符时,启用或禁用最佳映射行为。
            public bool BestFitMapping;
            //
            // 摘要:
            //     指示入口点的调用约定。
            public CallingConvention CallingConvention;
            //
            // 摘要:
            //     指示如何向方法封送字符串参数,并控制名称重整。
            public CharSet CharSet;
            //
            // 摘要:
            //     指示要调用的 DLL 入口点的名称或序号。
            public string EntryPoint;
            //
            // 摘要:
            //     控制 System.Runtime.InteropServices.DllImportAttribute.CharSet 字段是否使公共语言运行时在非托管
            //     DLL 中搜索入口点名称,而不使用指定的入口点名称。
            public bool ExactSpelling;
            //
            // 摘要:
            //     指示是否直接转换具有 HRESULT 或 retval 返回值的非托管方法,或是否自动将 HRESULT 或 retval 返回值转换为异常。
            public bool PreserveSig;
            //
            // 摘要:
            //     指示被调用方在从特性化方法返回之前是否调用 SetLastError Win32 API 函数。
            public bool SetLastError;
            //
            // 摘要:
            //     启用或禁止在遇到被转换为 ANSI“?”字符的无法映射的 Unicode 字符时引发异常。字符。
            public bool ThrowOnUnmappableChar;
    
            // 摘要:
            //     使用包含要导入的方法的 DLL 的名称初始化 System.Runtime.InteropServices.DllImportAttribute
            //     类的新实例。
            //
            // 参数:
            //   dllName:
            //     包含非托管方法的 DLL 的名称。如果 DLL 包含在某个程序集中,则可以包含程序集显示名称。
            public DllImportAttribute(string dllName);
    
            // 摘要:
            //     获取包含入口点的 DLL 文件的名称。
            //
            // 返回结果:
            //     包含入口点的 DLL 文件的名称。
            public string Value { get; }
        }

    从这个特性的定义中,我们可以看到以下几个特点

    1、DllImport仅可作用于方法的声明   ——由AttributeTargets.Method决定

    2、DllImport必须包含引入的DLL名称——由构造函数定义

    3、DllImport声明的方法 必须使用extern关键字 表示这是一个外部实现的方法

    另外这个特性的命名参数都有自己的默认值,在不进行特别声明的情况下,会使用默认值

    1、CallingConvention ,默认为CallingConvention.Winapi

    2、CharSet  ,默认为CharSet.Auto

    3、EntryPoint ,默认为方法本身的名称。

    4、ExactSpelling ,默认值为False

    5、PreserveSig ,默认值为True

    6、SetLastError  ,默认值为False

     

    在使用DllImoprt是,我们除了要提供带有入口点的DLL名称,还经常会用到CallingConvention 、CharSet 、EntryPoint 这三个命名参数。并且DllImport寻找文件的方式为1、在程序运行目录中寻找。2、从System32中寻找。3、从环境变量的定义中寻找。因此无论放在这三个地方哪个地方,都可以保证正常的引入API。

    下面我们用代码说话:

    C++中的定义:

    VIDEO_DEVIDE_LIBDLL int DeviceLogin(char* strDeviceIp, short m_siDevicePort, char* strUser, char* strPassword); 
    
    VIDEO_DEVIDE_LIBDLL void DeviceLogout();
    

    在引入函数的时候、我们要特别注意C++与C#的类型转换,具体的类型转换关系没有整理,网上有很多资料。如果类型对不上,会抛出异常。

    C#中引入:

     [DllImport(@"CPPSDKS\VideoDevSDK.dll", CallingConvention = CallingConvention.Cdecl, CharSet = CharSet.Ansi)]
     public static extern int DeviceLogin(string strDeviceIp, short m_siDevicePort, string strUser, string strPassword);
    
      
     [DllImport(@"CPPSDKS\VideoDevSDK.dll")]
     public static extern void DeviceLogout();

    大家仔细观察,会发现有参的方法需要对CallingConvention进行显式的设置。如果使用默认值 也就是Winapi定义的话,在调用这个方法的时候会抛出一个异常:对 PInvoke 函数“DemeterVideo.SDK!DemeterVideo.SDK.HKVersionSDK::DeviceLogin”的调用导致堆栈不对称。原因可能是托管的 PInvoke 签名与非托管的目标签名不匹配。请检查 PInvoke 签名的调用约定和参数与非托管的目标签名是否匹配。至于原因,由于是第一次接触,也不是很明了。而无参方法貌似就没有这么多的限制,这个问题头疼了很久,希望能为有需要的朋友提供一份便利。

    一般的函数调用,在提供足够明确的标识之后,一般不会出现什么大的问题,但是我们不得不正视另一个问题,那就是回调。回调这个概念,在C#里体现形式就是“委托“,他们的目的都是一样的,传递方法。在C++里 应该就是传递一个方法的指针。

    我们来看看代码:

    C++

    //结构
    typedef struct{ long nWidth; long nHeight; long nStamp; long nType; long nFrameRate; DWORD dwFrameNum; }AMG_FRAME_INFO; //回调的函数类型的定义 typedef void(CALLBACK *fVideoHandle)(long nPort, void* pBuf,long nSize, AMG_FRAME_INFO * pFrameInfo, long nReserved1,long nReserved2); //设置回调 VIDEO_DEVIDE_LIBDLL void SetVideoHandleCallback(fVideoHandle fVideo);

    这个回调的函数类型为fVideoHandle。这个措辞,我实在无法选择用一个准确的词汇进行描述,但是这个对象,在C#里它就是一个委托类型,它定义了可以接受的方法的定义。在这里,C++还定义了一个结构AMG_FRAME_INFO做为参数,这里可是一个赤裸裸的大坑。

    我们来看C#代码吧,C++的真的太别扭了

    /// <summary>
        /// 回调委托的参数之一
        /// </summary>
        public struct AMG_FRAME_INFO
        {
            /// <summary>
            /// 宽度
            /// </summary>
            public int nWidth;
            /// <summary>
            /// 高度
            /// </summary>
            public int nHeight;
            /// <summary>
            /// 
            /// </summary>
            public int nStamp;
            /// <summary>
            /// 
            /// </summary>
            public int nType;
            /// <summary>
            /// 
            /// </summary>
            public int nFrameRate;
            /// <summary>
            /// 
            /// </summary>
            public uint dwFrameNum;
        }
    //定义委托 建议显式指定与非托管代码交互的约定
    [UnmanagedFunctionPointer(CallingConvention.Cdecl)]
        public delegate void fVideoHandle(int nPort, IntPtr pBuf, int nSize, ref AMG_FRAME_INFO pFrameInfo, int nReserved1, int nReserved2);
    
    
     [DllImport(@"CPPSDKS\VideoDevSDK.dll", CallingConvention = CallingConvention.Cdecl, CharSet = CharSet.Ansi)]
            public static extern void SetVideoHandleCallback(fVideoHandle fVideo);

    我们先来看设置回调的方法,在这个方法中,参数是我们定义好的委托fVideohandle。其它与一个普通的方法调用没有什么区别。

    回过头来,我们看委托的定义。

    在这个委托的定义中,我们使用了复杂类型,一个结构。由于我们的代码是托管的,因此无法直接访问非托管代码中定义的结构,我们需要创建自己的结构。并且在使用这个结构做为参数时,我们必须使用ref关键字,表示传递过去的不是这个结构的值得副本,而是一个引用的地址,这样C++才能对这个结构进行赋值操作。

    说到这里,一些基本的调用方式就大概说完了。但是我们需要正视一个很严峻的问题,我们调用的代码是非托管代码,是UnSafe的,它们的内存需要自行处理、回收。但我们不行,我们的身后有位默默的清道夫——GC,垃圾回收机制,又称高潮兄。每当我们把内存占用推向一个又一个的高峰的时候,GC同志总是默默的回收那些没有被引用的对象。。。这是一件很伟大的事情,它让我们不用去考虑如何处理内存中的垃圾。然而在与非托管代码交互的时候,我们的蛋开始无休止的痛了。这种现象最常发生于传说中的”委托“中,刚我们也说了,委托的目的是用来传递方法,怎么传递呢?很明显,把方法的地址传过去呗。因此我们在对非托管代码设置回调的时候,实际是把我们的方法的内存地址传递给了非托管代码。非托管代码会通过这个地址,找到我们定义的方法,并且调用它。

    我们知道,GC的运行是非常随机的,他的目的也很明确,收破烂咯。我们的委托,在把地址传递给非托管代码之后,就没有任何地方再引用它了,那么等待它的命运只有一个,被回收!这时候异常就产生了。下面我们来看看这个蛋疼的异常描述:

    对“DemeterVideo.SDK!DemeterVideo.SDK.fVideoHandle::Invoke”类型的已垃圾回收委托进行了回调。这可能会导致应用程序崩溃、损坏和数据丢失。向非托管代码传递委托时,托管应用程序必须让这些委托保持活动状态,直到确信不会再次调用它们。

    这个异常产生的原因很简单,我偷懒了我在设置回调的时候直接写了下面这个代码

     public void SetCallback()
            {
                HKVersionSDK.SetVideoHandleCallback(new fVideoHandle(callback));
            }

    当这个方法执行完毕后,这个委托就会面临一个很尴尬的问题 生命周期完毕,等待被回收。

    那么这个问题怎么解决呢?全局变量。

    设置一个与类型生命周期相同的全局变量,这样在这个类型生命周期走完最后一程之前,这个委托会坚强的活下来……

    public class Sample
    {
      fVideoHanle videoCallback;
      
      publick void SetCallback()
      {
               callback = new fVideoHandle(OnStartPlaying);
    
                HKVersionSDK.SetVideoHandleCallback(Callback);
      }
    
      protected void Callback(...)
     {
      }
    }

    这个问题相信也会有很多人无奈的碰到。因为我们把地址给过去了,但是处于当前地址的方法被回收了的话,C++那边就无法找到位于该地址的方法了,因此会抛出异常,这就是原因。

    大概总结性的东西也就这么多了,其实没啥技术含量,都是马虎所致,但是碰到了,确实很恶心,并且答案还很不好找……

  • 相关阅读:
    20165227 结对编程项目-四则运算 第二周
    第八周学习总结
    20165227 结对编程项目-四则运算 第一周
    20165304第4次实验《Android程序设计》实验报告
    20165304《Java程序设计》第九周学习总结
    20165304实验三
    结对编程练习_四则运算(第二周)
    20165304 实验二 Java面向对象程序设计
    20165304 四则运算
    20165304《Java程序设计》第七周学习总结
  • 原文地址:https://www.cnblogs.com/ShadowLoki/p/2652341.html
Copyright © 2020-2023  润新知