分享

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

 雪柳花明 2016-08-18

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

最近在搞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++那边就无法找到位于该地址的方法了,因此会抛出异常,这就是原因。

 

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

    本站是提供个人知识管理的网络存储空间,所有内容均由用户发布,不代表本站观点。请注意甄别内容中的联系方式、诱导购买等信息,谨防诈骗。如发现有害或侵权内容,请点击一键举报。
    转藏 分享 献花(0

    0条评论

    发表

    请遵守用户 评论公约

    类似文章 更多